From d1c3643574d846ba2d63994cdef4d3642730c8b8 Mon Sep 17 00:00:00 2001 From: trisua Date: Wed, 16 Jul 2025 20:18:39 -0400 Subject: [PATCH] add: user ban_reason --- crates/app/src/langs/en-US.toml | 1 + crates/app/src/macros.rs | 5 ++- crates/app/src/public/html/components.lisp | 6 +++- crates/app/src/public/html/mod/profile.lisp | 29 +++++++++++++++- crates/app/src/public/html/root.lisp | 9 +++-- crates/app/src/routes/api/v1/auth/profile.rs | 34 +++++++++++++++++-- crates/app/src/routes/api/v1/mod.rs | 9 +++++ crates/core/src/database/auth.rs | 5 ++- .../src/database/drivers/sql/create_users.sql | 5 ++- crates/core/src/model/auth.rs | 4 +++ sql_changes/users_ban_reason.sql | 2 ++ 11 files changed, 100 insertions(+), 9 deletions(-) create mode 100644 sql_changes/users_ban_reason.sql diff --git a/crates/app/src/langs/en-US.toml b/crates/app/src/langs/en-US.toml index de5a411..226b35c 100644 --- a/crates/app/src/langs/en-US.toml +++ b/crates/app/src/langs/en-US.toml @@ -198,6 +198,7 @@ version = "1.0.0" "mod_panel:label.associations" = "Associations" "mod_panel:label.invited_by" = "Invited by" "mod_panel:label.send_debug_payload" = "Send debug payload" +"mod_panel:label.ban_reason" = "Ban reason" "mod_panel:action.send" = "Send" "requests:label.requests" = "Requests" diff --git a/crates/app/src/macros.rs b/crates/app/src/macros.rs index 2f5433d..b9faeb6 100644 --- a/crates/app/src/macros.rs +++ b/crates/app/src/macros.rs @@ -87,7 +87,10 @@ macro_rules! get_user_from_token { { Ok(ua) => { if ua.permissions.check_banned() { - Some(tetratto_core::model::auth::User::banned()) + let mut banned_user = tetratto_core::model::auth::User::banned(); + banned_user.ban_reason = ua.ban_reason; + + Some(banned_user) } else { Some(ua) } diff --git a/crates/app/src/public/html/components.lisp b/crates/app/src/public/html/components.lisp index 1f62828..53ef6d3 100644 --- a/crates/app/src/public/html/components.lisp +++ b/crates/app/src/public/html/components.lisp @@ -102,7 +102,11 @@ ("class" "flush") ("style" "font-weight: 600") ("target" "_top") - (text "{{ self::username(user=user) }}")) + (text "{% if user.permissions|has_banned -%}") + (del ("class" "fade") (text "{{ self::username(user=user) }}")) + (text "{% else %}") + (text "{{ self::username(user=user) }}") + (text "{%- endif %}")) (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}") (span ("title" "Verified") diff --git a/crates/app/src/public/html/mod/profile.lisp b/crates/app/src/public/html/mod/profile.lisp index 408b391..1d1410a 100644 --- a/crates/app/src/public/html/mod/profile.lisp +++ b/crates/app/src/public/html/mod/profile.lisp @@ -84,7 +84,7 @@ const ui = await ns(\"ui\"); const element = document.getElementById(\"mod_options\"); - async function profile_request(do_confirm, path, body) { + globalThis.profile_request = async (do_confirm, path, body) => { if (do_confirm) { if ( !(await trigger(\"atto::confirm\", [ @@ -273,6 +273,33 @@ ("class" "card lowered flex flex-wrap gap-2") (text "{{ components::user_plate(user=invite[0], show_menu=false) }}"))) (text "{%- endif %}") + (div + ("class" "card-nest w-full") + (div + ("class" "card small flex items-center justify-between gap-2") + (div + ("class" "flex items-center gap-2") + (icon (text "scale")) + (span + (str (text "mod_panel:label.ban_reason"))))) + (form + ("class" "card flex flex-col gap-2") + ("onsubmit" "event.preventDefault(); profile_request(false, 'ban_reason', { reason: event.target.reason.value || '' })") + (div + ("class" "flex flex-col gap-1") + (label + ("for" "title") + (str (text "mod_panel:label.ban_reason"))) + (textarea + ("type" "text") + ("name" "reason") + ("id" "reason") + ("placeholder" "ban reason") + ("minlength" "2") + (text "{{ profile.ban_reason|remove_script_tags|safe }}"))) + (button + ("class" "primary") + (str (text "general:action.save"))))) (div ("class" "card-nest w-full") (div diff --git a/crates/app/src/public/html/root.lisp b/crates/app/src/public/html/root.lisp index 6730dd8..3dd15a0 100644 --- a/crates/app/src/public/html/root.lisp +++ b/crates/app/src/public/html/root.lisp @@ -70,8 +70,13 @@ (str (text "general:label.account_banned"))) (div - ("class" "card") - (str (text "general:label.account_banned_body")))))) + ("class" "card flex flex-col gap-2 no_p_margin") + (str (text "general:label.account_banned_body")) + (hr) + (span ("class" "fade") (text "The following reason was provided by a moderator:")) + (div + ("class" "card lowered w-full") + (text "{{ user.ban_reason|markdown|safe }}")))))) ; if we aren't banned, just show the page body (text "{% elif user and user.awaiting_purchase %}") diff --git a/crates/app/src/routes/api/v1/auth/profile.rs b/crates/app/src/routes/api/v1/auth/profile.rs index bb874fe..5119e0d 100644 --- a/crates/app/src/routes/api/v1/auth/profile.rs +++ b/crates/app/src/routes/api/v1/auth/profile.rs @@ -4,8 +4,9 @@ use crate::{ model::{ApiReturn, Error}, routes::api::v1::{ AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken, - UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserInviteCode, - UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername, + UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserBanReason, + UpdateUserInviteCode, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, + UpdateUserUsername, }, State, }; @@ -424,6 +425,35 @@ pub async fn update_user_secondary_role_request( } } +/// Update the ban reason of the given user. +/// +/// Does not support third-party grants. +pub async fn update_user_ban_reason_request( + jar: CookieJar, + Path(id): Path, + Extension(data): Extension, + Json(req): Json, +) -> impl IntoResponse { + let data = &(data.read().await).0; + let user = match get_user_from_token!(jar, data) { + Some(ua) => ua, + None => return Json(Error::NotAllowed.into()), + }; + + if !user.permissions.check(FinePermission::MANAGE_USERS) { + return Json(Error::NotAllowed.into()); + } + + match data.update_user_ban_reason(id, &req.reason).await { + Ok(_) => Json(ApiReturn { + ok: true, + message: "User updated".to_string(), + payload: (), + }), + Err(e) => Json(e.into()), + } +} + /// Update the current user's last seen value. pub async fn seen_request(jar: CookieJar, Extension(data): Extension) -> impl IntoResponse { let data = &(data.read().await).0; diff --git a/crates/app/src/routes/api/v1/mod.rs b/crates/app/src/routes/api/v1/mod.rs index d59fe26..588a08e 100644 --- a/crates/app/src/routes/api/v1/mod.rs +++ b/crates/app/src/routes/api/v1/mod.rs @@ -322,6 +322,10 @@ pub fn routes() -> Router { "/auth/user/{id}/role/2", post(auth::profile::update_user_secondary_role_request), ) + .route( + "/auth/user/{id}/ban_reason", + post(auth::profile::update_user_ban_reason_request), + ) .route( "/auth/user/{id}", delete(auth::profile::delete_user_request), @@ -840,6 +844,11 @@ pub struct UpdateSecondaryUserRole { pub role: SecondaryPermission, } +#[derive(Deserialize)] +pub struct UpdateUserBanReason { + pub reason: String, +} + #[derive(Deserialize)] pub struct UpdateUserInviteCode { pub invite_code: String, diff --git a/crates/core/src/database/auth.rs b/crates/core/src/database/auth.rs index b8651ca..65c5307 100644 --- a/crates/core/src/database/auth.rs +++ b/crates/core/src/database/auth.rs @@ -119,6 +119,7 @@ impl DataManager { was_purchased: get!(x->25(i32)) as i8 == 1, browser_session: get!(x->26(String)), seller_data: serde_json::from_str(&get!(x->27(String)).to_string()).unwrap(), + ban_reason: get!(x->28(String)), } } @@ -275,7 +276,7 @@ impl DataManager { let res = execute!( &conn, - "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28)", + "INSERT INTO users VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24, $25, $26, $27, $28, $29)", params![ &(data.id as i64), &(data.created as i64), @@ -305,6 +306,7 @@ impl DataManager { &if data.was_purchased { 1_i32 } else { 0_i32 }, &data.browser_session, &serde_json::to_string(&data.seller_data).unwrap(), + &data.ban_reason ] ); @@ -1001,6 +1003,7 @@ impl DataManager { auto_method!(update_user_invite_code(i64)@get_user_by_id -> "UPDATE users SET invite_code = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(update_user_browser_session(&str)@get_user_by_id -> "UPDATE users SET browser_session = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(update_user_seller_data(StripeSellerData)@get_user_by_id -> "UPDATE users SET seller_data = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user); + auto_method!(update_user_ban_reason(&str)@get_user_by_id -> "UPDATE users SET ban_reason = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); auto_method!(get_user_by_stripe_id(&str)@get_user_from_row -> "SELECT * FROM users WHERE stripe_id = $1" --name="user" --returns=User); auto_method!(update_user_stripe_id(&str)@get_user_by_id -> "UPDATE users SET stripe_id = $1 WHERE id = $2" --cache-key-tmpl=cache_clear_user); diff --git a/crates/core/src/database/drivers/sql/create_users.sql b/crates/core/src/database/drivers/sql/create_users.sql index 0e24753..57b2078 100644 --- a/crates/core/src/database/drivers/sql/create_users.sql +++ b/crates/core/src/database/drivers/sql/create_users.sql @@ -20,9 +20,12 @@ CREATE TABLE IF NOT EXISTS users ( stripe_id TEXT NOT NULL, grants TEXT NOT NULL, associated TEXT NOT NULL, + invite_code TEXT NOT NULL, secondary_permissions INT NOT NULL, achievements TEXT NOT NULL, awaiting_purchase INT NOT NULL, was_purchased INT NOT NULL, - browser_session TEXT NOT NULL + browser_session TEXT NOT NULL, + seller_data TEXT NOT NULL, + ban_reason TEXT NOT NULL ) diff --git a/crates/core/src/model/auth.rs b/crates/core/src/model/auth.rs index 4fb1882..a9dadf1 100644 --- a/crates/core/src/model/auth.rs +++ b/crates/core/src/model/auth.rs @@ -83,6 +83,9 @@ pub struct User { /// Stripe connected account information (for Tetratto marketplace). #[serde(default)] pub seller_data: StripeSellerData, + /// The reason the user was banned. + #[serde(default)] + pub ban_reason: String, } pub type UserConnections = @@ -383,6 +386,7 @@ impl User { was_purchased: false, browser_session: String::new(), seller_data: StripeSellerData::default(), + ban_reason: String::new(), } } diff --git a/sql_changes/users_ban_reason.sql b/sql_changes/users_ban_reason.sql new file mode 100644 index 0000000..15f7b6f --- /dev/null +++ b/sql_changes/users_ban_reason.sql @@ -0,0 +1,2 @@ +ALTER TABLE users +ADD COLUMN ban_reason TEXT NOT NULL DEFAULT '';