From 4960527af3d9d8a2ac60cf8a7f9b7453986979e6 Mon Sep 17 00:00:00 2001 From: Bad Manners Date: Fri, 11 Apr 2025 23:50:28 -0300 Subject: [PATCH] Basic settings support --- README.md | 2 +- src/config.rs | 1 + src/lib.rs | 21 +++++- src/views.rs | 144 ++++++++++++++++++++++++++++++++++++--- templates/index.html | 5 +- templates/pool.html | 3 +- templates/pools.html | 3 +- templates/posts.html | 3 +- templates/settings.html | 24 +++++++ templates/view_post.html | 3 +- 10 files changed, 189 insertions(+), 20 deletions(-) create mode 100644 src/config.rs create mode 100644 templates/settings.html diff --git a/README.md b/README.md index dc69d7d..5e9fb00 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Sam's small image board. Currently a WIP. ## TODO -- [ ] Config - [ ] Video support - [ ] Cleanup/fixup background tasks +- [ ] User management - [ ] CSS - [ ] Cleanup, CLI, env vars, logging, better errors... diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..664dfaf --- /dev/null +++ b/src/config.rs @@ -0,0 +1 @@ +pub(crate) const APPLICATION_NAME_KEY: &str = "APPLICATION_NAME"; diff --git a/src/lib.rs b/src/lib.rs index a8e007a..dfef02f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub(crate) mod auth; +pub(crate) mod config; pub(crate) mod entities; pub(crate) mod error; pub(crate) mod query; @@ -13,20 +14,22 @@ use axum::{ routing::{delete, get, post, put}, }; use axum_login::AuthManagerLayerBuilder; +use entities::{prelude::SameyConfig, samey_config}; use password_auth::generate_hash; -use sea_orm::{ActiveValue::Set, DatabaseConnection, EntityTrait}; -use tokio::fs; +use sea_orm::{ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use tokio::{fs, sync::RwLock}; use tower_http::services::ServeDir; use tower_sessions::SessionManagerLayer; use crate::auth::{Backend, SessionStorage}; +use crate::config::APPLICATION_NAME_KEY; use crate::entities::{prelude::SameyUser, samey_user}; pub use crate::error::SameyError; use crate::views::{ add_post_source, add_post_to_pool, change_pool_visibility, create_pool, delete_post, edit_post_details, get_full_media, get_media, get_pools, get_pools_page, index, login, logout, post_details, posts, posts_page, remove_field, remove_pool_post, search_tags, select_tag, - sort_pool, submit_post_details, upload, view_pool, view_post, + settings, sort_pool, submit_post_details, update_settings, upload, view_pool, view_post, }; pub(crate) const NEGATIVE_PREFIX: &str = "-"; @@ -36,6 +39,7 @@ pub(crate) const RATING_PREFIX: &str = "rating:"; pub(crate) struct AppState { files_dir: Arc, db: DatabaseConnection, + application_name: Arc>, } pub async fn create_user( @@ -56,9 +60,18 @@ pub async fn create_user( } pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result { + let application_name = match SameyConfig::find() + .filter(samey_config::Column::Key.eq(APPLICATION_NAME_KEY)) + .one(&db) + .await? + { + Some(row) => row.data.as_str().unwrap_or("Samey").to_owned(), + None => "Samey".to_owned(), + }; let state = AppState { files_dir: Arc::new(files_dir.into()), db: db.clone(), + application_name: Arc::new(RwLock::new(application_name)), }; fs::create_dir_all(files_dir).await?; @@ -96,6 +109,8 @@ pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result, } -pub(crate) async fn index(auth_session: AuthSession) -> Result { +pub(crate) async fn index( + State(AppState { + application_name, .. + }): State, + auth_session: AuthSession, +) -> Result { + let application_name = application_name.read().await.clone(); Ok(Html( IndexTemplate { + application_name, user: auth_session.user, } .render()?, @@ -85,7 +99,7 @@ pub(crate) async fn logout(mut auth_session: AuthSession) -> Result, + State(AppState { db, files_dir, .. }): State, auth_session: AuthSession, mut multipart: Multipart, ) -> Result { @@ -370,6 +384,7 @@ pub(crate) async fn select_tag( #[derive(Template)] #[template(path = "posts.html")] struct PostsTemplate<'a> { + application_name: String, tags: Option>, tags_text: Option, posts: Vec, @@ -391,11 +406,16 @@ pub(crate) async fn posts( } pub(crate) async fn posts_page( - State(AppState { db, .. }): State, + State(AppState { + db, + application_name, + .. + }): State, auth_session: AuthSession, Query(query): Query, Path(page): Path, ) -> Result { + let application_name = application_name.read().await.clone(); let tags = query .tags .as_ref() @@ -417,6 +437,7 @@ pub(crate) async fn posts_page( Ok(Html( PostsTemplate { + application_name, tags_text: tags.as_ref().map(|tags| tags.iter().join(" ")), tags, posts, @@ -439,16 +460,22 @@ pub(crate) async fn get_pools( #[derive(Template)] #[template(path = "pools.html")] struct GetPoolsTemplate { + application_name: String, pools: Vec, page: u32, page_count: u64, } pub(crate) async fn get_pools_page( - State(AppState { db, .. }): State, + State(AppState { + db, + application_name, + .. + }): State, auth_session: AuthSession, Path(page): Path, ) -> Result { + let application_name = application_name.read().await.clone(); let query = match auth_session.user { None => SameyPool::find().filter(samey_pool::Column::IsPublic.into_simple_expr()), Some(user) if user.is_admin => SameyPool::find(), @@ -466,6 +493,7 @@ pub(crate) async fn get_pools_page( Ok(Html( GetPoolsTemplate { + application_name, pools, page, page_count, @@ -504,16 +532,22 @@ pub(crate) async fn create_pool( #[derive(Template)] #[template(path = "pool.html")] struct ViewPoolTemplate { + application_name: String, pool: samey_pool::Model, posts: Vec, can_edit: bool, } pub(crate) async fn view_pool( - State(AppState { db, .. }): State, + State(AppState { + db, + application_name, + .. + }): State, auth_session: AuthSession, Path(pool_id): Path, ) -> Result { + let application_name = application_name.read().await.clone(); let pool = SameyPool::find_by_id(pool_id) .one(&db) .await? @@ -534,6 +568,7 @@ pub(crate) async fn view_pool( Ok(Html( ViewPoolTemplate { + application_name, pool, can_edit, posts, @@ -763,11 +798,93 @@ pub(crate) async fn sort_pool( )) } +// Settings views + +#[derive(Template)] +#[template(path = "settings.html")] +struct SettingsTemplate { + application_name: String, +} + +pub(crate) async fn settings( + State(AppState { + db, + application_name, + .. + }): State, + auth_session: AuthSession, +) -> Result { + if auth_session.user.is_none_or(|user| !user.is_admin) { + return Err(SameyError::Forbidden); + } + + let application_name = application_name.read().await.clone(); + + let config = SameyConfig::find().all(&db).await?; + + let values: HashMap<&str, Box> = config + .iter() + .filter_map(|row| match row.key.as_str() { + key if key == APPLICATION_NAME_KEY => row + .data + .as_str() + .map::<(&str, Box), _>(|data| (&row.key, Box::new(data.to_owned()))), + _ => None, + }) + .collect(); + + Ok(Html( + SettingsTemplate { application_name }.render_with_values(&values)?, + )) +} + +#[derive(Debug, Deserialize)] +pub(crate) struct UpdateSettingsForm { + application_name: String, +} + +pub(crate) async fn update_settings( + State(AppState { + db, + application_name, + .. + }): State, + auth_session: AuthSession, + Form(body): Form, +) -> Result { + if auth_session.user.is_none_or(|user| !user.is_admin) { + return Err(SameyError::Forbidden); + } + + let mut configs = vec![]; + + if !body.application_name.is_empty() { + *application_name.write().await = body.application_name.clone(); + configs.push(samey_config::ActiveModel { + key: Set(APPLICATION_NAME_KEY.into()), + data: Set(body.application_name.into()), + ..Default::default() + }); + } + + SameyConfig::insert_many(configs) + .on_conflict( + OnConflict::column(samey_config::Column::Key) + .update_column(samey_config::Column::Data) + .to_owned(), + ) + .exec(&db) + .await?; + + Ok("") +} + // Single post views #[derive(Template)] #[template(path = "view_post.html")] struct ViewPostTemplate { + application_name: String, post: samey_post::Model, tags: Vec, tags_text: String, @@ -778,10 +895,16 @@ struct ViewPostTemplate { } pub(crate) async fn view_post( - State(AppState { db, .. }): State, + State(AppState { + db, + application_name, + .. + }): State, auth_session: AuthSession, Path(post_id): Path, ) -> Result { + let application_name = application_name.read().await.clone(); + let post_id = post_id; let tags = get_tags_for_post(post_id).all(&db).await?; let tags_text = tags.iter().map(|tag| &tag.name).join(" "); @@ -851,6 +974,7 @@ pub(crate) async fn view_post( Ok(Html( ViewPostTemplate { + application_name, post, tags, tags_text, @@ -1195,7 +1319,7 @@ pub(crate) async fn get_full_media( } pub(crate) async fn delete_post( - State(AppState { db, files_dir }): State, + State(AppState { db, files_dir, .. }): State, auth_session: AuthSession, Path(post_id): Path, ) -> Result { diff --git a/templates/index.html b/templates/index.html index d59142f..6c3adef 100644 --- a/templates/index.html +++ b/templates/index.html @@ -4,11 +4,12 @@ - Samey + {{ application_name }} +
-

Samey

+

{{ application_name }}

Search

diff --git a/templates/pool.html b/templates/pool.html index 4dc02c5..4b688eb 100644 --- a/templates/pool.html +++ b/templates/pool.html @@ -5,7 +5,8 @@ - Pool - {{ pool.name }} - Samey + Pool - {{ pool.name }} - {{ application_name }} + diff --git a/templates/pools.html b/templates/pools.html index ce499f5..3a4dd46 100644 --- a/templates/pools.html +++ b/templates/pools.html @@ -4,7 +4,8 @@ - Pools - Samey + Pools - {{ application_name }} +
diff --git a/templates/posts.html b/templates/posts.html index 2dc2348..cebaafb 100644 --- a/templates/posts.html +++ b/templates/posts.html @@ -4,7 +4,8 @@ - Posts - Samey + Posts - {{ application_name }} +
diff --git a/templates/settings.html b/templates/settings.html new file mode 100644 index 0000000..3dcf2da --- /dev/null +++ b/templates/settings.html @@ -0,0 +1,24 @@ + + + + + + + Settings - {{ application_name }} + + + +
+

Settings

+ + + + + +
+ + diff --git a/templates/view_post.html b/templates/view_post.html index 87b5d47..13c5b0f 100644 --- a/templates/view_post.html +++ b/templates/view_post.html @@ -4,7 +4,8 @@ - Post #{{ post.id }} - Samey + Post #{{ post.id }} - {{ application_name }} +