Skip to main content

max / makenotwork

5.2 KB · 195 lines History Blame Raw
1 //! Acting on an account: suspension, the appeal against it, and the two trust
2 //! flags a moderator can set.
3
4 use sqlx::PgPool;
5
6 use crate::db::UserId;
7 use crate::db::enums::AppealDecision;
8 use crate::db::models::DbUser;
9 use crate::error::Result;
10
11 /// Suspend a user account, clearing any prior appeal fields.
12 ///
13 /// Bumps `jwt_invalidated_at` so SyncKit JWTs minted for this user expire
14 /// at the next extractor check (subject to `SESSION_TOUCH_CACHE_SECS`).
15 #[tracing::instrument(skip_all)]
16 pub async fn suspend_user(pool: &PgPool, user_id: UserId, reason: &str) -> Result<()> {
17 sqlx::query(
18 r"
19 UPDATE users
20 SET suspended_at = NOW(),
21 suspension_reason = $2,
22 jwt_invalidated_at = NOW(),
23 appeal_text = NULL,
24 appeal_submitted_at = NULL,
25 appeal_decision = NULL,
26 appeal_response = NULL,
27 appeal_decided_at = NULL,
28 updated_at = NOW()
29 WHERE id = $1
30 ",
31 )
32 .bind(user_id)
33 .bind(reason)
34 .execute(pool)
35 .await?;
36
37 Ok(())
38 }
39
40 /// Remove suspension and clear all suspension/appeal fields.
41 #[tracing::instrument(skip_all)]
42 pub async fn unsuspend_user(pool: &PgPool, user_id: UserId) -> Result<()> {
43 sqlx::query(
44 r"
45 UPDATE users
46 SET suspended_at = NULL,
47 suspension_reason = NULL,
48 appeal_text = NULL,
49 appeal_submitted_at = NULL,
50 appeal_decision = NULL,
51 appeal_response = NULL,
52 appeal_decided_at = NULL,
53 updated_at = NOW()
54 WHERE id = $1
55 ",
56 )
57 .bind(user_id)
58 .execute(pool)
59 .await?;
60
61 Ok(())
62 }
63
64 /// Submit an appeal for a suspended account, clearing any prior decision.
65 #[tracing::instrument(skip_all)]
66 pub async fn submit_appeal(pool: &PgPool, user_id: UserId, appeal_text: &str) -> Result<()> {
67 sqlx::query(
68 r"
69 UPDATE users
70 SET appeal_text = $2,
71 appeal_submitted_at = NOW(),
72 appeal_decision = NULL,
73 appeal_response = NULL,
74 appeal_decided_at = NULL,
75 updated_at = NOW()
76 WHERE id = $1 AND suspended_at IS NOT NULL
77 ",
78 )
79 .bind(user_id)
80 .bind(appeal_text)
81 .execute(pool)
82 .await?;
83
84 Ok(())
85 }
86
87 /// Resolve an appeal. If approved, also clears suspension.
88 #[tracing::instrument(skip_all)]
89 pub async fn resolve_appeal(
90 pool: &PgPool,
91 user_id: UserId,
92 decision: AppealDecision,
93 response: &str,
94 ) -> Result<()> {
95 if decision == AppealDecision::Approved {
96 // Approve: clear suspension entirely
97 sqlx::query(
98 r"
99 UPDATE users
100 SET appeal_decision = $2,
101 appeal_response = $3,
102 appeal_decided_at = NOW(),
103 suspended_at = NULL,
104 suspension_reason = NULL,
105 updated_at = NOW()
106 WHERE id = $1
107 ",
108 )
109 .bind(user_id)
110 .bind(decision)
111 .bind(response)
112 .execute(pool)
113 .await?;
114 } else {
115 // Deny: keep suspension, record decision
116 sqlx::query(
117 r"
118 UPDATE users
119 SET appeal_decision = $2,
120 appeal_response = $3,
121 appeal_decided_at = NOW(),
122 updated_at = NOW()
123 WHERE id = $1
124 ",
125 )
126 .bind(user_id)
127 .bind(decision)
128 .bind(response)
129 .execute(pool)
130 .await?;
131 }
132
133 Ok(())
134 }
135
136 /// Admin query: users with a pending appeal (submitted but not yet decided).
137 #[tracing::instrument(skip_all)]
138 pub async fn get_pending_appeals(pool: &PgPool) -> Result<Vec<DbUser>> {
139 let users = sqlx::query_as::<_, DbUser>(
140 r"
141 SELECT * FROM users
142 WHERE appeal_submitted_at IS NOT NULL
143 AND appeal_decided_at IS NULL
144 ORDER BY appeal_submitted_at ASC
145 LIMIT 500
146 ",
147 )
148 .fetch_all(pool)
149 .await?;
150
151 Ok(users)
152 }
153
154 /// Check if a user is trusted for uploads (bypasses review queue).
155 #[tracing::instrument(skip_all)]
156 pub async fn is_upload_trusted(pool: &PgPool, user_id: UserId) -> Result<bool> {
157 let trusted = sqlx::query_scalar::<_, bool>("SELECT upload_trusted FROM users WHERE id = $1")
158 .bind(user_id)
159 .fetch_one(pool)
160 .await?;
161
162 Ok(trusted)
163 }
164
165 /// Set a user's upload trust status.
166 #[tracing::instrument(skip_all)]
167 pub async fn set_upload_trusted(pool: &PgPool, user_id: UserId, trusted: bool) -> Result<()> {
168 sqlx::query(
169 r"
170 UPDATE users
171 SET upload_trusted = $2,
172 updated_at = NOW()
173 WHERE id = $1
174 ",
175 )
176 .bind(user_id)
177 .bind(trusted)
178 .execute(pool)
179 .await?;
180
181 Ok(())
182 }
183
184 /// Moderation kill switch for custom pages. While locked, the creator can't
185 /// edit their custom pages and the live pages render the platform default.
186 /// Reversible: unlocking restores the (preserved) custom source.
187 pub async fn set_custom_pages_locked(pool: &PgPool, user_id: UserId, locked: bool) -> Result<()> {
188 sqlx::query("UPDATE users SET custom_pages_locked = $2, cache_generation = cache_generation + 1 WHERE id = $1")
189 .bind(user_id)
190 .bind(locked)
191 .execute(pool)
192 .await?;
193 Ok(())
194 }
195