AAAAAAAAAAAAAAAAAAAAAAAAAAA

This commit is contained in:
Hamcha 2023-06-29 14:08:59 +02:00
parent 06e8494c01
commit 8edc1dd15f
Signed by: hamcha
GPG key ID: 1669C533B8CF6D89
13 changed files with 235 additions and 41 deletions

13
Cargo.lock generated
View file

@ -132,6 +132,18 @@ dependencies = [
"tower-service",
]
[[package]]
name = "axum-macros"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bb524613be645939e280b7279f7b017f98cf7f5ef084ec374df373530e73277"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.18",
]
[[package]]
name = "base64"
version = "0.13.1"
@ -655,6 +667,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"axum-macros",
"chrono",
"figment",
"serde",

View file

@ -5,6 +5,7 @@ edition = "2021"
[dependencies]
axum = "0.6"
axum-macros = "0.3"
tracing = "0.1"
tracing-subscriber = "0.3"
tokio = { version = "1.28", features = ["full"] }

View file

@ -0,0 +1,5 @@
DROP TABLE pages;
DROP TABLE sites;
DROP TABLE audit;
DROP TABLE users;
DROP TABLE roles;

View file

@ -0,0 +1,52 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (),
name VARCHAR UNIQUE NOT NULL,
email VARCHAR,
password BYTEA,
display_name VARCHAR,
bio TEXT,
roles UUID[] NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now (),
modified_at TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE roles (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (),
scopes VARCHAR[] NOT NULL
);
CREATE TABLE sites (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (),
owner UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
name VARCHAR UNIQUE NOT NULL,
title VARCHAR NOT NULL,
description TEXT,
created_at TIMESTAMP NOT NULL DEFAULT now (),
modified_at TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE pages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4 (),
site UUID NOT NULL REFERENCES sites(id) ON DELETE CASCADE,
author UUID REFERENCES users(id) ON DELETE SET NULL,
slug VARCHAR NOT NULL,
title VARCHAR NOT NULL,
description TEXT,
tags VARCHAR[] NOT NULL,
blocks JSONB NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now (),
modified_at TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE TABLE audit (
actor UUID REFERENCES users(id) ON DELETE SET NULL,
object UUID,
action VARCHAR NOT NULL,
data JSONB,
created_at TIMESTAMP
);

View file

@ -1 +0,0 @@
DROP TABLE pages;

View file

@ -1,12 +0,0 @@
CREATE TABLE pages (
id UUID PRIMARY KEY,
author UUID NOT NULL,
slug VARCHAR NOT NULL,
title VARCHAR NOT NULL,
description TEXT,
tags VARCHAR[] NOT NULL,
blocks JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
modified_at TIMESTAMPTZ,
deleted_at TIMESTAMPTZ
);

View file

@ -3,11 +3,81 @@ use serde::{Deserialize, Serialize};
use sqlx::{types::Json, FromRow};
use uuid::Uuid;
#[derive(Deserialize, Serialize, FromRow)]
pub struct User {
/// User internal ID
pub id: Uuid,
/// User name (unique per instance, shows up in URLs)
pub name: String,
/// User email (for validation/login)
pub email: Option<String>,
/// Hashed password
pub password: Option<Vec<u8>>,
/// User's chosen displayed name
pub display_name: Option<String>,
/// Biography / User description
pub bio: Option<String>,
/// User roles (as role IDs)
pub roles: Vec<Uuid>,
/// Times
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
#[serde(with = "ts_seconds_option")]
pub modified_at: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")]
pub deleted_at: Option<DateTime<Utc>>,
}
#[derive(Deserialize, Serialize, FromRow)]
pub struct Role {
/// Role ID
pub id: Uuid,
/// Role scopes (permissions)
pub scopes: Vec<String>,
}
#[derive(Deserialize, Serialize, FromRow)]
pub struct Site {
/// Site internal ID
pub id: Uuid,
/// Site owner (user)
pub owner: Uuid,
/// Site name (unique per instance, shows up in URLs)
pub name: String,
/// Site's displayed name
pub title: String,
/// Site description (like a user's bio)
pub description: Option<String>,
/// Times
#[serde(with = "ts_seconds")]
pub created_at: DateTime<Utc>,
#[serde(with = "ts_seconds_option")]
pub modified_at: Option<DateTime<Utc>>,
#[serde(with = "ts_seconds_option")]
pub deleted_at: Option<DateTime<Utc>>,
}
#[derive(Deserialize, Serialize, FromRow)]
pub struct Page {
/// Page ID
pub id: Uuid,
/// Site ID
pub site: Uuid,
/// Author ID
pub author: Uuid,

View file

@ -1,23 +1,41 @@
use anyhow::anyhow;
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
pub struct InternalError(anyhow::Error);
pub enum AppError {
ServerError(anyhow::Error),
ClientError {
status: StatusCode,
code: String,
message: String,
},
}
impl IntoResponse for InternalError {
impl IntoResponse for AppError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
match self {
AppError::ServerError(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({"code":"server-error", "message": err.to_string()})),
)
.into_response(),
AppError::ClientError {
status,
code,
message,
} => (status, Json(json!({"code":code, "message": message}))).into_response(),
}
}
}
impl From<sqlx::Error> for InternalError {
fn from(value: sqlx::Error) -> Self {
InternalError(anyhow!("Database error: {}", value))
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
AppError::ServerError(err.into())
}
}

View file

@ -1,19 +1,22 @@
mod content;
mod error;
mod routes;
mod state;
use anyhow::Result;
use axum::{extract::State, routing::get, Router, Server};
use error::InternalError;
use axum::{
routing::{get, post},
Router, Server,
};
use figment::{
providers::{Env, Format, Serialized, Toml},
Figment,
};
use serde::{Deserialize, Serialize};
use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
use sqlx::postgres::PgPoolOptions;
use state::AppState;
use std::{net::SocketAddr, sync::Arc};
use crate::content::Page;
#[derive(Deserialize, Serialize)]
struct Config {
bind: String,
@ -29,17 +32,6 @@ impl Default for Config {
}
}
struct AppState {
database: Pool<Postgres>,
}
async fn root(State(state): State<Arc<AppState>>) -> Result<String, InternalError> {
let select_query: sqlx::query::QueryAs<'_, _, Page, _> =
sqlx::query_as::<_, Page>("SELECT * FROM pages");
let page: Page = select_query.fetch_one(&state.database).await?;
Ok(page.title)
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
@ -58,7 +50,10 @@ async fn main() -> Result<()> {
let shared_state = Arc::new(AppState { database: pool });
let app = Router::new().route("/", get(root)).with_state(shared_state);
let app = Router::new()
.route("/pages/:site/:slug", get(routes::content::page))
.route("/admin/bootstrap", post(routes::admin::bootstrap))
.with_state(shared_state);
let addr: SocketAddr = config.bind.parse()?;
tracing::debug!("listening on {}", addr);

29
src/routes/admin.rs Normal file
View file

@ -0,0 +1,29 @@
use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use crate::error::AppError;
use crate::state::AppState;
pub async fn bootstrap(State(state): State<Arc<AppState>>) -> Result<(), AppError> {
// Only allow this request if the user db is completely empty!
let empty = sqlx::query!(
"SELECT CASE WHEN EXISTS(SELECT 1 FROM users) THEN false ELSE true END AS empty;"
)
.map(|row| row.empty.unwrap_or(true))
.fetch_one(&state.database)
.await?;
match empty {
false => Err(AppError::ClientError {
status: StatusCode::BAD_REQUEST,
code: "already-setup".to_string(),
message: "The instance was already bootstrapped".to_string(),
}),
true => {
//todo add user
Ok(())
}
}
}

17
src/routes/content.rs Normal file
View file

@ -0,0 +1,17 @@
use std::sync::Arc;
use crate::{content::Page, error::AppError, state::AppState};
use axum::extract::{Path, State};
pub async fn page(
State(state): State<Arc<AppState>>,
Path((site, slug)): Path<(String, String)>,
) -> Result<String, AppError> {
let page_query = sqlx::query_as(
"SELECT p.* FROM pages p JOIN sites s ON p.site = s.id WHERE p.slug = $1 AND s.name = $2",
)
.bind(slug)
.bind(site);
let page: Page = page_query.fetch_one(&state.database).await?;
Ok(page.title)
}

2
src/routes/mod.rs Normal file
View file

@ -0,0 +1,2 @@
pub mod admin;
pub mod content;

5
src/state.rs Normal file
View file

@ -0,0 +1,5 @@
use sqlx::{Pool, Postgres};
pub struct AppState {
pub database: Pool<Postgres>,
}