diff --git a/migration/src/lib.rs b/migration/src/lib.rs index c2dc720..54daf58 100644 --- a/migration/src/lib.rs +++ b/migration/src/lib.rs @@ -12,6 +12,7 @@ mod m20260517_000007_audio_tags; mod m20260517_000008_audio_track_tags; mod m20260517_000009_simple_constraints; mod m20260517_000010_drop_user_roles; +mod m20260517_000011_site_pages; pub struct Migrator; @@ -30,6 +31,7 @@ impl MigratorTrait for Migrator { Box::new(m20260517_000008_audio_track_tags::Migration), Box::new(m20260517_000009_simple_constraints::Migration), Box::new(m20260517_000010_drop_user_roles::Migration), + Box::new(m20260517_000011_site_pages::Migration), // inject-above (do not remove this comment) ] } diff --git a/migration/src/m20260517_000011_site_pages.rs b/migration/src/m20260517_000011_site_pages.rs new file mode 100644 index 0000000..f0eb15b --- /dev/null +++ b/migration/src/m20260517_000011_site_pages.rs @@ -0,0 +1,65 @@ +use sea_orm_migration::{prelude::*, sea_orm::ConnectionTrait, sea_query::Expr}; + +#[derive(DeriveMigrationName)] +pub struct Migration; + +#[derive(DeriveIden)] +enum SitePages { + Table, + Id, + Slug, + Title, + Content, + CreatedAt, + UpdatedAt, +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> { + m.create_table( + Table::create() + .table(SitePages::Table) + .if_not_exists() + .col(ColumnDef::new(SitePages::Id).uuid().not_null().primary_key()) + .col( + ColumnDef::new(SitePages::Slug) + .string_len(100) + .not_null() + .unique_key(), + ) + .col(ColumnDef::new(SitePages::Title).string_len(500).not_null()) + .col(ColumnDef::new(SitePages::Content).text().not_null()) + .col( + ColumnDef::new(SitePages::CreatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .col( + ColumnDef::new(SitePages::UpdatedAt) + .timestamp_with_time_zone() + .not_null() + .default(Expr::current_timestamp()), + ) + .to_owned(), + ) + .await?; + + m.get_connection() + .execute_unprepared( + "INSERT INTO site_pages (id, slug, title, content, created_at, updated_at) \ + VALUES (gen_random_uuid(), 'about', 'About', '', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) \ + ON CONFLICT (slug) DO NOTHING", + ) + .await?; + + Ok(()) + } + + async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> { + m.drop_table(Table::drop().table(SitePages::Table).to_owned()) + .await?; + Ok(()) + } +} diff --git a/src/app.rs b/src/app.rs index d03d951..f2234e2 100644 --- a/src/app.rs +++ b/src/app.rs @@ -54,6 +54,7 @@ impl Hooks for App { .add_route(controllers::auth::routes()) .add_route(controllers::admin::routes()) .add_route(controllers::blog::routes()) + .add_route(controllers::pages::routes()) } async fn connect_workers(ctx: &AppContext, queue: &Queue) -> Result<()> { queue.register(DownloadWorker::build(ctx)).await?; diff --git a/src/controllers/mod.rs b/src/controllers/mod.rs index 5d28ec9..12be272 100644 --- a/src/controllers/mod.rs +++ b/src/controllers/mod.rs @@ -1,3 +1,4 @@ pub mod admin; pub mod auth; pub mod blog; +pub mod pages; diff --git a/src/controllers/pages.rs b/src/controllers/pages.rs new file mode 100644 index 0000000..a2133fc --- /dev/null +++ b/src/controllers/pages.rs @@ -0,0 +1,89 @@ +use crate::{ + controllers::admin, + models::_entities::site_pages, +}; +use loco_rs::prelude::*; +use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +const ABOUT_SLUG: &str = "about"; + +#[derive(Debug, Deserialize)] +struct AboutParams { + title: String, + content: String, +} + +#[derive(Debug, Serialize)] +struct PageResponse { + id: Uuid, + slug: String, + title: String, + content: String, + updated_at: chrono::DateTime, +} + +impl From for PageResponse { + fn from(page: site_pages::Model) -> Self { + Self { + id: page.id, + slug: page.slug, + title: page.title, + content: page.content, + updated_at: page.updated_at, + } + } +} + +async fn find_about(ctx: &AppContext) -> Result { + site_pages::Entity::find() + .filter(site_pages::Column::Slug.eq(ABOUT_SLUG)) + .one(&ctx.db) + .await? + .ok_or_else(|| Error::NotFound) +} + +#[debug_handler] +async fn about(State(ctx): State) -> Result { + format::json(PageResponse::from(find_about(&ctx).await?)) +} + +#[debug_handler] +async fn update_about( + auth: auth::JWT, + State(ctx): State, + Json(params): Json, +) -> Result { + admin::current_admin(auth, &ctx).await?; + + let page = match find_about(&ctx).await { + Ok(page) => { + let mut page = page.into_active_model(); + page.title = Set(params.title); + page.content = Set(params.content); + page.update(&ctx.db).await? + } + Err(Error::NotFound) => { + site_pages::ActiveModel { + id: Set(Uuid::new_v4()), + slug: Set(ABOUT_SLUG.to_string()), + title: Set(params.title), + content: Set(params.content), + ..Default::default() + } + .insert(&ctx.db) + .await? + } + Err(err) => return Err(err), + }; + + format::json(PageResponse::from(page)) +} + +pub fn routes() -> Routes { + Routes::new() + .prefix("/api") + .add("/about", get(about)) + .add("/admin/about", put(update_about)) +} diff --git a/src/models/_entities/mod.rs b/src/models/_entities/mod.rs index 1df2611..2d97cfa 100644 --- a/src/models/_entities/mod.rs +++ b/src/models/_entities/mod.rs @@ -8,4 +8,5 @@ pub mod audio_track_tags; pub mod audio_tracks; pub mod audit_logs; pub mod blog_articles; +pub mod site_pages; pub mod users; diff --git a/src/models/_entities/prelude.rs b/src/models/_entities/prelude.rs index c1e363c..694ea74 100644 --- a/src/models/_entities/prelude.rs +++ b/src/models/_entities/prelude.rs @@ -6,4 +6,5 @@ pub use super::audio_track_tags::Entity as AudioTrackTags; pub use super::audio_tracks::Entity as AudioTracks; pub use super::audit_logs::Entity as AuditLogs; pub use super::blog_articles::Entity as BlogArticles; +pub use super::site_pages::Entity as SitePages; pub use super::users::Entity as Users; diff --git a/src/models/_entities/site_pages.rs b/src/models/_entities/site_pages.rs new file mode 100644 index 0000000..6b6b15b --- /dev/null +++ b/src/models/_entities/site_pages.rs @@ -0,0 +1,21 @@ +//! `SeaORM` Entity, @generated by sea-orm-codegen 1.1.20 + +use sea_orm::entity::prelude::*; +use serde::{Deserialize, Serialize}; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)] +#[sea_orm(table_name = "site_pages")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + #[sea_orm(unique)] + pub slug: String, + pub title: String, + #[sea_orm(column_type = "Text")] + pub content: String, + pub created_at: DateTimeWithTimeZone, + pub updated_at: DateTimeWithTimeZone, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} diff --git a/src/models/mod.rs b/src/models/mod.rs index b516710..79f8cfa 100644 --- a/src/models/mod.rs +++ b/src/models/mod.rs @@ -6,3 +6,4 @@ pub mod audio_track_tags; pub mod audit_logs; pub mod blog_articles; pub mod audio_albums; +pub mod site_pages; diff --git a/src/models/site_pages.rs b/src/models/site_pages.rs new file mode 100644 index 0000000..74ac47e --- /dev/null +++ b/src/models/site_pages.rs @@ -0,0 +1,23 @@ +use sea_orm::entity::prelude::*; +pub use super::_entities::site_pages::{ActiveModel, Entity, Model}; +pub type SitePages = Entity; + +#[async_trait::async_trait] +impl ActiveModelBehavior for ActiveModel { + async fn before_save(self, _db: &C, insert: bool) -> std::result::Result + where + C: ConnectionTrait, + { + if !insert && self.updated_at.is_unchanged() { + let mut this = self; + this.updated_at = sea_orm::ActiveValue::Set(chrono::Utc::now().into()); + Ok(this) + } else { + Ok(self) + } + } +} + +impl Model {} +impl ActiveModel {} +impl Entity {}