kept you waiting huh? (I'm sorry i forgor)
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
parent
18c9be898a
commit
58bf506240
20 changed files with 342 additions and 121 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -761,6 +761,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"argon2",
|
"argon2",
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"axum-macros",
|
"axum-macros",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
|
|
@ -20,6 +20,7 @@ anyhow = "1.0"
|
||||||
argon2 = { version = "0.5", features = ["std", "alloc"] }
|
argon2 = { version = "0.5", features = ["std", "alloc"] }
|
||||||
url = "2.4"
|
url = "2.4"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
|
async-trait = "0.1"
|
||||||
|
|
||||||
[profile.dev.package.sqlx-macros]
|
[profile.dev.package.sqlx-macros]
|
||||||
opt-level = 3
|
opt-level = 3
|
|
@ -1,4 +1,3 @@
|
||||||
pub mod hash;
|
pub mod hash;
|
||||||
pub mod http;
|
|
||||||
pub mod session;
|
pub mod session;
|
||||||
pub mod user;
|
pub mod user;
|
||||||
|
|
|
@ -137,13 +137,6 @@ impl Session {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn user(self: &Self, pool: &Pool<Postgres>) -> Result<User, ApiError<'static>> {
|
|
||||||
match User::get_id(pool, self.actor).await? {
|
|
||||||
Some(user) => Ok(user),
|
|
||||||
None => Err(USER_NOT_FOUND),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn destroy(self: Self, pool: &Pool<Postgres>) -> Result<()> {
|
pub async fn destroy(self: Self, pool: &Pool<Postgres>) -> Result<()> {
|
||||||
sqlx::query!("DELETE FROM sessions WHERE id = $1", self.id)
|
sqlx::query!("DELETE FROM sessions WHERE id = $1", self.id)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
|
|
|
@ -76,13 +76,6 @@ impl User {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_id(pool: &Pool<Postgres>, id: Uuid) -> Result<Option<Self>> {
|
|
||||||
Ok(sqlx::query_as("SELECT * FROM users WHERE id = $1")
|
|
||||||
.bind(id)
|
|
||||||
.fetch_optional(pool)
|
|
||||||
.await?)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn find(pool: &Pool<Postgres>, name: &str) -> Result<Option<Self>> {
|
pub async fn find(pool: &Pool<Postgres>, name: &str) -> Result<Option<Self>> {
|
||||||
Ok(sqlx::query_as("SELECT * FROM users WHERE name = $1")
|
Ok(sqlx::query_as("SELECT * FROM users WHERE name = $1")
|
||||||
.bind(name)
|
.bind(name)
|
||||||
|
|
14
src/content/mod.rs
Normal file
14
src/content/mod.rs
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
pub mod page;
|
||||||
|
pub mod site;
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error("Resource not found")]
|
||||||
|
NotFound,
|
||||||
|
|
||||||
|
#[error("Resource identifier not available")]
|
||||||
|
IdentifierNotAvailable,
|
||||||
|
|
||||||
|
#[error("Database error: {0}")]
|
||||||
|
DatabaseError(#[from] sqlx::Error),
|
||||||
|
}
|
|
@ -1,30 +1,10 @@
|
||||||
use chrono::{serde::*, DateTime, NaiveDateTime, Utc};
|
use async_trait::async_trait;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sqlx::{types::Json, FromRow};
|
use sqlx::{types::Json, FromRow};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, FromRow)]
|
use super::Error;
|
||||||
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
|
|
||||||
pub created_at: NaiveDateTime,
|
|
||||||
pub modified_at: Option<NaiveDateTime>,
|
|
||||||
pub deleted_at: Option<NaiveDateTime>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, FromRow)]
|
#[derive(Deserialize, Serialize, FromRow)]
|
||||||
pub struct Page {
|
pub struct Page {
|
||||||
|
@ -58,6 +38,11 @@ pub struct Page {
|
||||||
pub deleted_at: Option<NaiveDateTime>,
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait PageRepository {
|
||||||
|
async fn get_page_from_url(&self, site: &str, slug: &str) -> Result<Page, Error>;
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize)]
|
#[derive(Deserialize, Serialize)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
pub enum PageBlock {
|
pub enum PageBlock {
|
75
src/content/site.rs
Normal file
75
src/content/site.rs
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use chrono::NaiveDateTime;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use sqlx::FromRow;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use super::Error;
|
||||||
|
|
||||||
|
#[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
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
pub modified_at: Option<NaiveDateTime>,
|
||||||
|
pub deleted_at: Option<NaiveDateTime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// More useful version of Site for showing to users
|
||||||
|
#[derive(Serialize)]
|
||||||
|
pub struct APISite {
|
||||||
|
pub name: String,
|
||||||
|
pub owner: String,
|
||||||
|
pub title: String,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub created_at: NaiveDateTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data required to create a new site
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct CreateSiteOptions {
|
||||||
|
pub name: String,
|
||||||
|
pub title: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data required to update a site's info
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct UpdateSiteOptions {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait SiteRepository {
|
||||||
|
/// Retrieve site info from site name/slug
|
||||||
|
async fn get_site_by_name(&self, name: &str) -> Result<APISite, Error>;
|
||||||
|
|
||||||
|
/// Create a new instance of a site and store it
|
||||||
|
async fn create_site(&self, owner: &Uuid, options: &CreateSiteOptions) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Update an existing site's info
|
||||||
|
async fn update_site(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
name: &str,
|
||||||
|
options: &UpdateSiteOptions,
|
||||||
|
) -> Result<(), Error>;
|
||||||
|
|
||||||
|
/// Delete a site
|
||||||
|
async fn delete_site(&self, owner: &Uuid, name: &str, soft_delete: bool) -> Result<(), Error>;
|
||||||
|
}
|
19
src/database/mod.rs
Normal file
19
src/database/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::state::AppState;
|
||||||
|
|
||||||
|
pub mod page;
|
||||||
|
pub mod site;
|
||||||
|
|
||||||
|
pub struct Repository {
|
||||||
|
pool: PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<&Arc<AppState>> for Repository {
|
||||||
|
fn from(state: &Arc<AppState>) -> Self {
|
||||||
|
Repository {
|
||||||
|
pool: state.database.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
src/database/page.rs
Normal file
26
src/database/page.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use crate::content::{
|
||||||
|
self,
|
||||||
|
page::{Page, PageRepository},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Repository;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl PageRepository for Repository {
|
||||||
|
async fn get_page_from_url(&self, site: &str, slug: &str) -> Result<Page, content::Error> {
|
||||||
|
let page_query = sqlx::query_as(
|
||||||
|
"SELECT pages.* FROM pages JOIN sites ON site = sites.id WHERE slug = $1 AND name = $2 AND pages.deleted_at IS NULL AND sites.deleted_at IS NULL")
|
||||||
|
.bind(slug)
|
||||||
|
.bind(site);
|
||||||
|
let page: Page = page_query
|
||||||
|
.fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
sqlx::Error::RowNotFound => content::Error::NotFound,
|
||||||
|
_ => e.into(),
|
||||||
|
})?;
|
||||||
|
Ok(page)
|
||||||
|
}
|
||||||
|
}
|
98
src/database/site.rs
Normal file
98
src/database/site.rs
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::content::{
|
||||||
|
site::{APISite, CreateSiteOptions, SiteRepository, UpdateSiteOptions},
|
||||||
|
Error,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Repository;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl SiteRepository for Repository {
|
||||||
|
async fn get_site_by_name(&self, name: &str) -> Result<APISite, Error> {
|
||||||
|
let site = sqlx::query_as!(
|
||||||
|
APISite,
|
||||||
|
"SELECT sites.name, users.name as owner, title, description, sites.created_at FROM sites JOIN users ON sites.owner = users.id WHERE sites.name = $1 AND sites.deleted_at IS NULL",
|
||||||
|
name
|
||||||
|
).fetch_one(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
sqlx::Error::RowNotFound => Error::NotFound,
|
||||||
|
_ => e.into(),
|
||||||
|
})?;
|
||||||
|
Ok(site)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn create_site(&self, owner: &Uuid, options: &CreateSiteOptions) -> Result<(), Error> {
|
||||||
|
sqlx::query!(
|
||||||
|
"INSERT INTO sites (name, owner, title) VALUES ($1, $2, $3)",
|
||||||
|
options.name,
|
||||||
|
owner,
|
||||||
|
options.title,
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|err| match err {
|
||||||
|
sqlx::Error::Database(dberr) if dberr.constraint() == Some("sites_name_key") => {
|
||||||
|
Error::IdentifierNotAvailable
|
||||||
|
}
|
||||||
|
_ => err.into(),
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_site(
|
||||||
|
&self,
|
||||||
|
owner: &Uuid,
|
||||||
|
name: &str,
|
||||||
|
options: &UpdateSiteOptions,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let result = sqlx::query!(
|
||||||
|
"UPDATE sites SET name = COALESCE($1, name), title = COALESCE($2, title), description = COALESCE($3, description), modified_at = NOW() WHERE name = $4 AND owner = $5 AND deleted_at IS NULL",
|
||||||
|
options.name,
|
||||||
|
options.title,
|
||||||
|
options.description,
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
).execute(&self.pool).await.map_err(|err| match err {
|
||||||
|
sqlx::Error::Database(dberr) if dberr.constraint() == Some("sites_name_key") => {
|
||||||
|
Error::IdentifierNotAvailable
|
||||||
|
}
|
||||||
|
_ => err.into(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(Error::NotFound.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn delete_site(&self, owner: &Uuid, name: &str, soft_delete: bool) -> Result<(), Error> {
|
||||||
|
let result = if soft_delete {
|
||||||
|
sqlx::query!(
|
||||||
|
// soft delete
|
||||||
|
"UPDATE sites SET deleted_at = NOW() WHERE name = $1 AND owner = $2 AND deleted_at IS NULL",
|
||||||
|
name, owner
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
sqlx::query!(
|
||||||
|
// hard delete
|
||||||
|
"DELETE FROM sites WHERE name = $1 AND owner = $2",
|
||||||
|
name,
|
||||||
|
owner,
|
||||||
|
)
|
||||||
|
.execute(&self.pool)
|
||||||
|
.await?
|
||||||
|
};
|
||||||
|
|
||||||
|
if result.rows_affected() == 0 {
|
||||||
|
return Err(Error::NotFound.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,12 +7,19 @@ use axum::{
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::content;
|
||||||
|
|
||||||
// Generic errors
|
// Generic errors
|
||||||
pub const ERR_NOT_FOUND: ApiError<'static> = ApiError::ClientError {
|
const ERR_NOT_FOUND: ApiError<'static> = ApiError::ClientError {
|
||||||
status: StatusCode::NOT_FOUND,
|
status: StatusCode::NOT_FOUND,
|
||||||
code: "not-found",
|
code: "not-found",
|
||||||
message: "resource not found",
|
message: "resource not found",
|
||||||
};
|
};
|
||||||
|
const ERR_NOT_AVAILABLE: ApiError<'static> = ApiError::ClientError {
|
||||||
|
status: StatusCode::CONFLICT,
|
||||||
|
code: "id-not-available",
|
||||||
|
message: "the chosen identifier is not available",
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum ApiError<'a> {
|
pub enum ApiError<'a> {
|
||||||
|
@ -67,3 +74,13 @@ impl IntoResponse for ApiError<'_> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<content::Error> for ApiError<'_> {
|
||||||
|
fn from(err: content::Error) -> Self {
|
||||||
|
match err {
|
||||||
|
content::Error::NotFound => ERR_NOT_FOUND,
|
||||||
|
content::Error::IdentifierNotAvailable => ERR_NOT_AVAILABLE,
|
||||||
|
content::Error::DatabaseError(err) => err.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,2 +1,4 @@
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod extract;
|
pub mod json;
|
||||||
|
pub mod repository;
|
||||||
|
pub mod session;
|
||||||
|
|
20
src/http/repository.rs
Normal file
20
src/http/repository.rs
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use axum::{extract::FromRequestParts, http::request::Parts};
|
||||||
|
|
||||||
|
use crate::{database::Repository, state::AppState};
|
||||||
|
|
||||||
|
use super::error::ApiError;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl FromRequestParts<Arc<AppState>> for Repository {
|
||||||
|
type Rejection = ApiError<'static>;
|
||||||
|
|
||||||
|
async fn from_request_parts(
|
||||||
|
_parts: &mut Parts,
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
) -> Result<Self, Self::Rejection> {
|
||||||
|
Ok(state.into())
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,9 +16,11 @@ use cookie::Cookie;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{http::error::ApiError, state::AppState};
|
use crate::{
|
||||||
|
auth::{session::Session, user::User},
|
||||||
use super::{session::Session, user::User};
|
http::error::ApiError,
|
||||||
|
state::AppState,
|
||||||
|
};
|
||||||
|
|
||||||
pub const INVALID_SESSION: ApiError = ApiError::ClientError {
|
pub const INVALID_SESSION: ApiError = ApiError::ClientError {
|
||||||
status: StatusCode::UNAUTHORIZED,
|
status: StatusCode::UNAUTHORIZED,
|
|
@ -1,11 +1,12 @@
|
||||||
mod auth;
|
mod auth;
|
||||||
mod content;
|
mod content;
|
||||||
|
mod database;
|
||||||
mod http;
|
mod http;
|
||||||
mod roles;
|
mod roles;
|
||||||
mod routes;
|
mod routes;
|
||||||
mod state;
|
mod state;
|
||||||
|
|
||||||
use crate::{auth::http::refresh_sessions, state::Config};
|
use crate::{http::session::refresh_sessions, state::Config};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use axum::{middleware, Server};
|
use axum::{middleware, Server};
|
||||||
use figment::{
|
use figment::{
|
||||||
|
|
|
@ -11,13 +11,12 @@ use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::{
|
auth::{hash::verify, session::Session, user::User},
|
||||||
hash::verify,
|
http::{
|
||||||
http::{RequireSession, RequireUser},
|
error::ApiError,
|
||||||
session::Session,
|
json::JsonBody,
|
||||||
user::User,
|
session::{RequireSession, RequireUser},
|
||||||
},
|
},
|
||||||
http::{error::ApiError, extract::JsonBody},
|
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -1,36 +1,35 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, State},
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::get,
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
content::Page,
|
content::page::PageRepository,
|
||||||
http::error::{ApiError, ERR_NOT_FOUND},
|
database::Repository,
|
||||||
|
http::{error::ApiError, session::RequireUser},
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn get_page(
|
async fn get_page(
|
||||||
State(state): State<Arc<AppState>>,
|
repository: Repository,
|
||||||
Path((site, slug)): Path<(String, String)>,
|
Path((site, slug)): Path<(String, String)>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
let page_query = sqlx::query_as(
|
Ok(Json(repository.get_page_from_url(&site, &slug).await?))
|
||||||
"SELECT p.* FROM pages p JOIN sites s ON p.site = s.id WHERE p.slug = $1 AND s.name = $2",
|
}
|
||||||
)
|
|
||||||
.bind(slug)
|
async fn create_page(
|
||||||
.bind(site);
|
State(state): State<Arc<AppState>>,
|
||||||
let page: Page = page_query
|
Path(site): Path<String>,
|
||||||
.fetch_one(&state.database)
|
RequireUser(user): RequireUser,
|
||||||
.await
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
.map_err(|e| match e {
|
Ok(Json("todo"))
|
||||||
sqlx::Error::RowNotFound => ERR_NOT_FOUND,
|
|
||||||
_ => e.into(),
|
|
||||||
})?;
|
|
||||||
Ok(Json(page))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new().route("/:site/:slug", get(get_page))
|
Router::new()
|
||||||
|
.route("/:site", post(create_page))
|
||||||
|
.route("/:site/:slug", get(get_page))
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,79 +1,56 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::Path,
|
||||||
http::StatusCode,
|
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{get, post},
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::http::RequireUser,
|
content::site::{CreateSiteOptions, SiteRepository, UpdateSiteOptions},
|
||||||
http::{
|
database::Repository,
|
||||||
error::{ApiError, ERR_NOT_FOUND},
|
http::{error::ApiError, json::JsonBody, session::RequireUser},
|
||||||
extract::JsonBody,
|
|
||||||
},
|
|
||||||
state::AppState,
|
state::AppState,
|
||||||
};
|
};
|
||||||
|
|
||||||
async fn get_site(
|
async fn get_site(
|
||||||
State(state): State<Arc<AppState>>,
|
repository: Repository,
|
||||||
Path(name): Path<String>,
|
Path(name): Path<String>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
let site = sqlx::query!(
|
Ok(Json(repository.get_site_by_name(&name).await?))
|
||||||
"SELECT sites.id, sites.name, users.name as owner, title, description, sites.created_at FROM sites JOIN users ON sites.owner = users.id WHERE sites.name = $1 ",
|
|
||||||
name
|
|
||||||
).fetch_one(&state.database)
|
|
||||||
.await
|
|
||||||
.map_err(|e| match e {
|
|
||||||
sqlx::Error::RowNotFound => ERR_NOT_FOUND,
|
|
||||||
_ => e.into(),
|
|
||||||
})?;
|
|
||||||
Ok(Json(json!({
|
|
||||||
"name": site.name,
|
|
||||||
"owner": site.owner,
|
|
||||||
"title": site.title,
|
|
||||||
"description": site.description,
|
|
||||||
"created_at": site.created_at,
|
|
||||||
})))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct CreateSiteRequest {
|
|
||||||
name: String,
|
|
||||||
title: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn create_site(
|
async fn create_site(
|
||||||
State(state): State<Arc<AppState>>,
|
repository: Repository,
|
||||||
RequireUser(user): RequireUser,
|
RequireUser(user): RequireUser,
|
||||||
JsonBody(payload): JsonBody<CreateSiteRequest>,
|
JsonBody(options): JsonBody<CreateSiteOptions>,
|
||||||
) -> Result<impl IntoResponse, ApiError<'static>> {
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
sqlx::query!(
|
repository.create_site(&user.id, &options).await?;
|
||||||
"INSERT INTO sites (name, owner, title) VALUES ($1, $2, $3)",
|
Ok(Json(json!({"ok": true})))
|
||||||
payload.name,
|
|
||||||
user.id,
|
|
||||||
payload.title,
|
|
||||||
)
|
|
||||||
.execute(&state.database)
|
|
||||||
.await
|
|
||||||
.map_err(|err| match err {
|
|
||||||
sqlx::Error::Database(dberr) if dberr.constraint() == Some("sites_name_key") => {
|
|
||||||
ApiError::ClientError {
|
|
||||||
status: StatusCode::CONFLICT,
|
|
||||||
code: "name-not-available",
|
|
||||||
message: "the chosen name is not available",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_site(
|
||||||
|
repository: Repository,
|
||||||
|
Path(name): Path<String>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
JsonBody(options): JsonBody<UpdateSiteOptions>,
|
||||||
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
|
repository.update_site(&user.id, &name, &options).await?;
|
||||||
|
Ok(Json(json!({"ok": true})))
|
||||||
}
|
}
|
||||||
_ => err.into(),
|
|
||||||
})?;
|
async fn delete_site(
|
||||||
Ok(Json(json!({"ok": true, "url": "test"})))
|
repository: Repository,
|
||||||
|
Path(name): Path<String>,
|
||||||
|
RequireUser(user): RequireUser,
|
||||||
|
) -> Result<impl IntoResponse, ApiError<'static>> {
|
||||||
|
repository.delete_site(&user.id, &name, true).await?;
|
||||||
|
Ok(Json(json!({"ok": true})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router() -> Router<Arc<AppState>> {
|
pub fn router() -> Router<Arc<AppState>> {
|
||||||
Router::new()
|
Router::new()
|
||||||
.route("/", post(create_site))
|
.route("/", post(create_site))
|
||||||
.route("/:name", get(get_site))
|
.route("/:name", get(get_site).put(update_site).delete(delete_site))
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue