add: temporary bans

This commit is contained in:
trisua 2025-08-05 13:39:01 -04:00
parent 9650c0177e
commit 155fe34c6e
11 changed files with 132 additions and 19 deletions

View file

@ -214,6 +214,7 @@ version = "1.0.0"
"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:label.ban_expiration" = "Ban expiration"
"requests:label.requests" = "Requests"
"requests:label.community_join_request" = "Community join request"

View file

@ -87,10 +87,31 @@ macro_rules! get_user_from_token {
{
Ok(ua) => {
if ua.permissions.check_banned() {
// check expiration
let now = tetratto_shared::unix_epoch_timestamp();
let expired = ua.ban_expire <= now;
if expired && ua.ban_expire != 0 {
$db.update_user_role(
ua.id,
ua.permissions
- tetratto_core::model::permissions::FinePermission::BANNED,
&ua,
true,
)
.await
.expect("failed to auto unban user");
Some(ua)
} else {
// banned
let mut banned_user = tetratto_core::model::auth::User::banned();
banned_user.ban_reason = ua.ban_reason;
banned_user.ban_expire = ua.ban_expire;
Some(banned_user)
}
} else {
Some(ua)
}

View file

@ -298,7 +298,7 @@
(div
("class" "flex flex_col gap_1")
(label
("for" "title")
("for" "reason")
(str (text "mod_panel:label.ban_reason")))
(textarea
("type" "text")
@ -309,6 +309,37 @@
(text "{{ profile.ban_reason|remove_script_tags|safe }}")))
(button
(str (text "general:action.save")))))
(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_expiration")))))
(form
("class" "card flex flex_col gap_2")
("onsubmit" "event.preventDefault(); profile_request(false, 'ban_expire', { expire: new Date(event.target.expire.value).getTime() || 0 })")
(div
("class" "flex flex_col gap_1")
(label
("for" "expire")
(str (text "mod_panel:label.ban_expiration")))
(input
("type" "datetime-local")
("name" "expire")
("id" "expire")
("value" "{{ profile.ban_expire }}")))
(div
("class" "flex gap_2")
(button
(str (text "general:action.save")))
(button
("type" "button")
("class" "lowered red")
("onclick" "profile_request(false, 'ban_expire', { expire: 0 })")
(str (text "notifs:action.clear"))))))
(div
("class" "card_nest w_full")
(div

View file

@ -76,7 +76,17 @@
(span ("class" "fade") (text "The following reason was provided by a moderator:"))
(div
("class" "card lowered w_full")
(text "{{ user.ban_reason|markdown|safe }}"))))))
(text "{{ user.ban_reason|markdown|safe }}"))
(text "{% if user.ban_expire != 0 -%}")
(hr)
(span
(text "Your ban will expire on: ")
(span ("id" "ban_expire")))
(script
(text "document.getElementById(\"ban_expire\").innerText = new Date({{ user.ban_expire }}).toLocaleString();"))
(text "{% else %}")
(span (text "This ban is marked as permanent."))
(text "{%- endif %}")))))
; if we aren't banned, just show the page body
(text "{% elif user and user.awaiting_purchase %}")
; account waiting for payment message

View file

@ -179,7 +179,7 @@ pub async fn stripe_webhook(
let new_user_permissions = user.permissions | FinePermission::SUPPORTER;
if let Err(e) = data
.update_user_role(user.id, new_user_permissions, user.clone(), true)
.update_user_role(user.id, new_user_permissions, &user, true)
.await
{
return Json(e.into());
@ -225,7 +225,7 @@ pub async fn stripe_webhook(
user.secondary_permissions | SecondaryPermission::DEVELOPER_PASS;
if let Err(e) = data
.update_user_secondary_role(user.id, new_user_permissions, user.clone(), true)
.update_user_secondary_role(user.id, new_user_permissions, &user, true)
.await
{
return Json(e.into());
@ -284,7 +284,7 @@ pub async fn stripe_webhook(
let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
if let Err(e) = data
.update_user_role(user.id, new_user_permissions, user.clone(), true)
.update_user_role(user.id, new_user_permissions, &user, true)
.await
{
return Json(e.into());
@ -310,7 +310,7 @@ pub async fn stripe_webhook(
user.secondary_permissions - SecondaryPermission::DEVELOPER_PASS;
if let Err(e) = data
.update_user_secondary_role(user.id, new_user_permissions, user.clone(), true)
.update_user_secondary_role(user.id, new_user_permissions, &user, true)
.await
{
return Json(e.into());
@ -396,7 +396,7 @@ pub async fn stripe_webhook(
let new_user_permissions = user.permissions - FinePermission::SUPPORTER;
if let Err(e) = data
.update_user_role(user.id, new_user_permissions, user.clone(), true)
.update_user_role(user.id, new_user_permissions, &user, true)
.await
{
return Json(e.into());
@ -437,7 +437,7 @@ pub async fn stripe_webhook(
user.secondary_permissions - SecondaryPermission::DEVELOPER_PASS;
if let Err(e) = data
.update_user_secondary_role(user.id, new_user_permissions, user.clone(), true)
.update_user_secondary_role(user.id, new_user_permissions, &user, true)
.await
{
return Json(e.into());

View file

@ -4,9 +4,9 @@ use crate::{
model::{ApiReturn, Error},
routes::api::v1::{
AppendAssociations, AwardAchievement, DeleteUser, DisableTotp, RefreshGrantToken,
UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserBanReason,
UpdateUserInviteCode, UpdateUserIsDeactivated, UpdateUserIsVerified, UpdateUserPassword,
UpdateUserRole, UpdateUserUsername,
UpdateSecondaryUserRole, UpdateUserAwaitingPurchase, UpdateUserBanExpire,
UpdateUserBanReason, UpdateUserInviteCode, UpdateUserIsDeactivated, UpdateUserIsVerified,
UpdateUserPassword, UpdateUserRole, UpdateUserUsername,
},
State,
};
@ -423,7 +423,7 @@ pub async fn update_user_role_request(
None => return Json(Error::NotAllowed.into()),
};
match data.update_user_role(id, req.role, user, false).await {
match data.update_user_role(id, req.role, &user, false).await {
Ok(_) => Json(ApiReturn {
ok: true,
message: "User updated".to_string(),
@ -449,7 +449,7 @@ pub async fn update_user_secondary_role_request(
};
match data
.update_user_secondary_role(id, req.role, user, false)
.update_user_secondary_role(id, req.role, &user, false)
.await
{
Ok(_) => Json(ApiReturn {
@ -490,6 +490,35 @@ pub async fn update_user_ban_reason_request(
}
}
/// Update the ban expiration date of the given user.
///
/// Does not support third-party grants.
pub async fn update_user_ban_expire_request(
jar: CookieJar,
Path(id): Path<usize>,
Extension(data): Extension<State>,
Json(req): Json<UpdateUserBanExpire>,
) -> 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_expire(id, req.expire as i64).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<State>) -> impl IntoResponse {
let data = &(data.read().await).0;

View file

@ -340,6 +340,10 @@ pub fn routes() -> Router {
"/auth/user/{id}/ban_reason",
post(auth::profile::update_user_ban_reason_request),
)
.route(
"/auth/user/{id}/ban_expire",
post(auth::profile::update_user_ban_expire_request),
)
.route(
"/auth/user/{id}",
delete(auth::profile::delete_user_request),
@ -916,6 +920,11 @@ pub struct UpdateUserBanReason {
pub reason: String,
}
#[derive(Deserialize)]
pub struct UpdateUserBanExpire {
pub expire: usize,
}
#[derive(Deserialize)]
pub struct UpdateUserInviteCode {
pub invite_code: String,

View file

@ -27,7 +27,7 @@ macro_rules! update_role_fn {
&self,
id: usize,
role: $role_ty,
user: User,
user: &User,
force: bool,
) -> Result<()> {
let other_user = self.get_user_by_id(id).await?;
@ -129,6 +129,7 @@ impl DataManager {
ban_reason: get!(x->28(String)),
channel_mutes: serde_json::from_str(&get!(x->29(String)).to_string()).unwrap(),
is_deactivated: get!(x->30(i32)) as i8 == 1,
ban_expire: get!(x->31(i64)) as usize,
}
}
@ -285,7 +286,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, $29, $30, $31)",
"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, $30, $31, $32)",
params![
&(data.id as i64),
&(data.created as i64),
@ -318,6 +319,7 @@ impl DataManager {
&data.ban_reason,
&serde_json::to_string(&data.channel_mutes).unwrap(),
&if data.is_deactivated { 1_i32 } else { 0_i32 },
&(data.ban_expire as i64),
]
);
@ -1059,6 +1061,7 @@ impl DataManager {
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!(update_user_channel_mutes(Vec<usize>)@get_user_by_id -> "UPDATE users SET channel_mutes = $1 WHERE id = $2" --serde --cache-key-tmpl=cache_clear_user);
auto_method!(update_user_ban_expire(i64)@get_user_by_id -> "UPDATE users SET ban_expire = $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);

View file

@ -29,5 +29,6 @@ CREATE TABLE IF NOT EXISTS users (
seller_data TEXT NOT NULL,
ban_reason TEXT NOT NULL,
channel_mutes TEXT NOT NULL,
is_deactivated INT NOT NULL
is_deactivated INT NOT NULL,
ban_expire BIGINT NOT NULL
)

View file

@ -25,3 +25,7 @@ ADD COLUMN IF NOT EXISTS topics TEXT DEFAULT '{}';
-- posts topic
ALTER TABLE posts
ADD COLUMN IF NOT EXISTS topic BIGINT DEFAULT 0;
-- users ban_expire
ALTER TABLE users
ADD COLUMN IF NOT EXISTS ban_expire BIGINT DEFAULT 0;

View file

@ -92,6 +92,9 @@ pub struct User {
/// users, but their data is not wiped.
#[serde(default)]
pub is_deactivated: bool,
/// The time at which the user's ban will automatically expire.
#[serde(default)]
pub ban_expire: usize,
}
pub type UserConnections =
@ -408,6 +411,7 @@ impl User {
ban_reason: String::new(),
channel_mutes: Vec::new(),
is_deactivated: false,
ban_expire: 0,
}
}