//! Acting on an account: suspension, the appeal against it, and the two trust //! flags a moderator can set. use sqlx::PgPool; use crate::db::UserId; use crate::db::enums::AppealDecision; use crate::db::models::DbUser; use crate::error::Result; /// Suspend a user account, clearing any prior appeal fields. /// /// Bumps `jwt_invalidated_at` so SyncKit JWTs minted for this user expire /// at the next extractor check (subject to `SESSION_TOUCH_CACHE_SECS`). #[tracing::instrument(skip_all)] pub async fn suspend_user(pool: &PgPool, user_id: UserId, reason: &str) -> Result<()> { sqlx::query( r" UPDATE users SET suspended_at = NOW(), suspension_reason = $2, jwt_invalidated_at = NOW(), appeal_text = NULL, appeal_submitted_at = NULL, appeal_decision = NULL, appeal_response = NULL, appeal_decided_at = NULL, updated_at = NOW() WHERE id = $1 ", ) .bind(user_id) .bind(reason) .execute(pool) .await?; Ok(()) } /// Remove suspension and clear all suspension/appeal fields. #[tracing::instrument(skip_all)] pub async fn unsuspend_user(pool: &PgPool, user_id: UserId) -> Result<()> { sqlx::query( r" UPDATE users SET suspended_at = NULL, suspension_reason = NULL, appeal_text = NULL, appeal_submitted_at = NULL, appeal_decision = NULL, appeal_response = NULL, appeal_decided_at = NULL, updated_at = NOW() WHERE id = $1 ", ) .bind(user_id) .execute(pool) .await?; Ok(()) } /// Submit an appeal for a suspended account, clearing any prior decision. #[tracing::instrument(skip_all)] pub async fn submit_appeal(pool: &PgPool, user_id: UserId, appeal_text: &str) -> Result<()> { sqlx::query( r" UPDATE users SET appeal_text = $2, appeal_submitted_at = NOW(), appeal_decision = NULL, appeal_response = NULL, appeal_decided_at = NULL, updated_at = NOW() WHERE id = $1 AND suspended_at IS NOT NULL ", ) .bind(user_id) .bind(appeal_text) .execute(pool) .await?; Ok(()) } /// Resolve an appeal. If approved, also clears suspension. #[tracing::instrument(skip_all)] pub async fn resolve_appeal( pool: &PgPool, user_id: UserId, decision: AppealDecision, response: &str, ) -> Result<()> { if decision == AppealDecision::Approved { // Approve: clear suspension entirely sqlx::query( r" UPDATE users SET appeal_decision = $2, appeal_response = $3, appeal_decided_at = NOW(), suspended_at = NULL, suspension_reason = NULL, updated_at = NOW() WHERE id = $1 ", ) .bind(user_id) .bind(decision) .bind(response) .execute(pool) .await?; } else { // Deny: keep suspension, record decision sqlx::query( r" UPDATE users SET appeal_decision = $2, appeal_response = $3, appeal_decided_at = NOW(), updated_at = NOW() WHERE id = $1 ", ) .bind(user_id) .bind(decision) .bind(response) .execute(pool) .await?; } Ok(()) } /// Admin query: users with a pending appeal (submitted but not yet decided). #[tracing::instrument(skip_all)] pub async fn get_pending_appeals(pool: &PgPool) -> Result> { let users = sqlx::query_as::<_, DbUser>( r" SELECT * FROM users WHERE appeal_submitted_at IS NOT NULL AND appeal_decided_at IS NULL ORDER BY appeal_submitted_at ASC LIMIT 500 ", ) .fetch_all(pool) .await?; Ok(users) } /// Check if a user is trusted for uploads (bypasses review queue). #[tracing::instrument(skip_all)] pub async fn is_upload_trusted(pool: &PgPool, user_id: UserId) -> Result { let trusted = sqlx::query_scalar::<_, bool>("SELECT upload_trusted FROM users WHERE id = $1") .bind(user_id) .fetch_one(pool) .await?; Ok(trusted) } /// Set a user's upload trust status. #[tracing::instrument(skip_all)] pub async fn set_upload_trusted(pool: &PgPool, user_id: UserId, trusted: bool) -> Result<()> { sqlx::query( r" UPDATE users SET upload_trusted = $2, updated_at = NOW() WHERE id = $1 ", ) .bind(user_id) .bind(trusted) .execute(pool) .await?; Ok(()) } /// Moderation kill switch for custom pages. While locked, the creator can't /// edit their custom pages and the live pages render the platform default. /// Reversible: unlocking restores the (preserved) custom source. pub async fn set_custom_pages_locked(pool: &PgPool, user_id: UserId, locked: bool) -> Result<()> { sqlx::query("UPDATE users SET custom_pages_locked = $2, cache_generation = cache_generation + 1 WHERE id = $1") .bind(user_id) .bind(locked) .execute(pool) .await?; Ok(()) }