add: user ban_reason

This commit is contained in:
trisua 2025-07-16 20:18:39 -04:00
parent b25bda29b8
commit d1c3643574
11 changed files with 100 additions and 9 deletions

View file

@ -198,6 +198,7 @@ version = "1.0.0"
"mod_panel:label.associations" = "Associations" "mod_panel:label.associations" = "Associations"
"mod_panel:label.invited_by" = "Invited by" "mod_panel:label.invited_by" = "Invited by"
"mod_panel:label.send_debug_payload" = "Send debug payload" "mod_panel:label.send_debug_payload" = "Send debug payload"
"mod_panel:label.ban_reason" = "Ban reason"
"mod_panel:action.send" = "Send" "mod_panel:action.send" = "Send"
"requests:label.requests" = "Requests" "requests:label.requests" = "Requests"

View file

@ -87,7 +87,10 @@ macro_rules! get_user_from_token {
{ {
Ok(ua) => { Ok(ua) => {
if ua.permissions.check_banned() { 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 { } else {
Some(ua) Some(ua)
} }

View file

@ -102,7 +102,11 @@
("class" "flush") ("class" "flush")
("style" "font-weight: 600") ("style" "font-weight: 600")
("target" "_top") ("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 -%}") (text "{{ self::online_indicator(user=user) }} {% if user.is_verified -%}")
(span (span
("title" "Verified") ("title" "Verified")

View file

@ -84,7 +84,7 @@
const ui = await ns(\"ui\"); const ui = await ns(\"ui\");
const element = document.getElementById(\"mod_options\"); 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 (do_confirm) {
if ( if (
!(await trigger(\"atto::confirm\", [ !(await trigger(\"atto::confirm\", [
@ -273,6 +273,33 @@
("class" "card lowered flex flex-wrap gap-2") ("class" "card lowered flex flex-wrap gap-2")
(text "{{ components::user_plate(user=invite[0], show_menu=false) }}"))) (text "{{ components::user_plate(user=invite[0], show_menu=false) }}")))
(text "{%- endif %}") (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 (div
("class" "card-nest w-full") ("class" "card-nest w-full")
(div (div

View file

@ -70,8 +70,13 @@
(str (text "general:label.account_banned"))) (str (text "general:label.account_banned")))
(div (div
("class" "card") ("class" "card flex flex-col gap-2 no_p_margin")
(str (text "general:label.account_banned_body")))))) (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 ; if we aren't banned, just show the page body
(text "{% elif user and user.awaiting_purchase %}") (text "{% elif user and user.awaiting_purchase %}")

View file

@ -4,8 +4,9 @@ use crate::{
model::{ApiReturn, Error}, model::{ApiReturn, Error},
routes::api::v1::{ routes::api::v1::{
AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken, AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken,
UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserInviteCode, UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserBanReason,
UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole, UpdateUserUsername, UpdateUserInviteCode, UpdateUserIsVerified, UpdateUserPassword, UpdateUserRole,
UpdateUserUsername,
}, },
State, 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<usize>,
Extension(data): Extension<State>,
Json(req): Json<UpdateUserBanReason>,
) -> 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. /// Update the current user's last seen value.
pub async fn seen_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse { pub async fn seen_request(jar: CookieJar, Extension(data): Extension<State>) -> impl IntoResponse {
let data = &(data.read().await).0; let data = &(data.read().await).0;

View file

@ -322,6 +322,10 @@ pub fn routes() -> Router {
"/auth/user/{id}/role/2", "/auth/user/{id}/role/2",
post(auth::profile::update_user_secondary_role_request), post(auth::profile::update_user_secondary_role_request),
) )
.route(
"/auth/user/{id}/ban_reason",
post(auth::profile::update_user_ban_reason_request),
)
.route( .route(
"/auth/user/{id}", "/auth/user/{id}",
delete(auth::profile::delete_user_request), delete(auth::profile::delete_user_request),
@ -840,6 +844,11 @@ pub struct UpdateSecondaryUserRole {
pub role: SecondaryPermission, pub role: SecondaryPermission,
} }
#[derive(Deserialize)]
pub struct UpdateUserBanReason {
pub reason: String,
}
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct UpdateUserInviteCode { pub struct UpdateUserInviteCode {
pub invite_code: String, pub invite_code: String,

View file

@ -119,6 +119,7 @@ impl DataManager {
was_purchased: get!(x->25(i32)) as i8 == 1, was_purchased: get!(x->25(i32)) as i8 == 1,
browser_session: get!(x->26(String)), browser_session: get!(x->26(String)),
seller_data: serde_json::from_str(&get!(x->27(String)).to_string()).unwrap(), 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!( let res = execute!(
&conn, &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![ params![
&(data.id as i64), &(data.id as i64),
&(data.created as i64), &(data.created as i64),
@ -305,6 +306,7 @@ impl DataManager {
&if data.was_purchased { 1_i32 } else { 0_i32 }, &if data.was_purchased { 1_i32 } else { 0_i32 },
&data.browser_session, &data.browser_session,
&serde_json::to_string(&data.seller_data).unwrap(), &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_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_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_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!(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); 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);

View file

@ -20,9 +20,12 @@ CREATE TABLE IF NOT EXISTS users (
stripe_id TEXT NOT NULL, stripe_id TEXT NOT NULL,
grants TEXT NOT NULL, grants TEXT NOT NULL,
associated TEXT NOT NULL, associated TEXT NOT NULL,
invite_code TEXT NOT NULL,
secondary_permissions INT NOT NULL, secondary_permissions INT NOT NULL,
achievements TEXT NOT NULL, achievements TEXT NOT NULL,
awaiting_purchase INT NOT NULL, awaiting_purchase INT NOT NULL,
was_purchased 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
) )

View file

@ -83,6 +83,9 @@ pub struct User {
/// Stripe connected account information (for Tetratto marketplace). /// Stripe connected account information (for Tetratto marketplace).
#[serde(default)] #[serde(default)]
pub seller_data: StripeSellerData, pub seller_data: StripeSellerData,
/// The reason the user was banned.
#[serde(default)]
pub ban_reason: String,
} }
pub type UserConnections = pub type UserConnections =
@ -383,6 +386,7 @@ impl User {
was_purchased: false, was_purchased: false,
browser_session: String::new(), browser_session: String::new(),
seller_data: StripeSellerData::default(), seller_data: StripeSellerData::default(),
ban_reason: String::new(),
} }
} }

View file

@ -0,0 +1,2 @@
ALTER TABLE users
ADD COLUMN ban_reason TEXT NOT NULL DEFAULT '';