this repo has no description
1use crate::state::AppState;
2use axum::{
3 Json,
4 extract::State,
5 http::StatusCode,
6 response::{IntoResponse, Response},
7};
8use chrono::{Duration, Utc};
9use serde::{Deserialize, Serialize};
10use serde_json::json;
11use tracing::{error, info, warn};
12use uuid::Uuid;
13
14#[derive(Serialize)]
15#[serde(rename_all = "camelCase")]
16pub struct CheckAccountStatusOutput {
17 pub activated: bool,
18 pub valid_did: bool,
19 pub repo_commit: String,
20 pub repo_rev: String,
21 pub repo_blocks: i64,
22 pub indexed_records: i64,
23 pub private_state_values: i64,
24 pub expected_blobs: i64,
25 pub imported_blobs: i64,
26}
27
28pub async fn check_account_status(
29 State(state): State<AppState>,
30 headers: axum::http::HeaderMap,
31) -> Response {
32 let auth_header = headers.get("Authorization");
33 if auth_header.is_none() {
34 return (
35 StatusCode::UNAUTHORIZED,
36 Json(json!({"error": "AuthenticationRequired"})),
37 )
38 .into_response();
39 }
40
41 let token = auth_header
42 .unwrap()
43 .to_str()
44 .unwrap_or("")
45 .replace("Bearer ", "");
46
47 let session = sqlx::query!(
48 r#"
49 SELECT s.did, k.key_bytes, u.id as user_id
50 FROM sessions s
51 JOIN users u ON s.did = u.did
52 JOIN user_keys k ON u.id = k.user_id
53 WHERE s.access_jwt = $1
54 "#,
55 token
56 )
57 .fetch_optional(&state.db)
58 .await;
59
60 let (did, key_bytes, user_id) = match session {
61 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id),
62 Ok(None) => {
63 return (
64 StatusCode::UNAUTHORIZED,
65 Json(json!({"error": "AuthenticationFailed"})),
66 )
67 .into_response();
68 }
69 Err(e) => {
70 error!("DB error in check_account_status: {:?}", e);
71 return (
72 StatusCode::INTERNAL_SERVER_ERROR,
73 Json(json!({"error": "InternalError"})),
74 )
75 .into_response();
76 }
77 };
78
79 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
80 return (
81 StatusCode::UNAUTHORIZED,
82 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
83 )
84 .into_response();
85 }
86
87 let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
88 .fetch_optional(&state.db)
89 .await;
90
91 let deactivated_at = match user_status {
92 Ok(Some(row)) => row.deactivated_at,
93 _ => None,
94 };
95
96 let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
97 .fetch_optional(&state.db)
98 .await;
99
100 let repo_commit = match repo_result {
101 Ok(Some(row)) => row.repo_root_cid,
102 _ => String::new(),
103 };
104
105 let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
106 .fetch_one(&state.db)
107 .await
108 .unwrap_or(Some(0))
109 .unwrap_or(0);
110
111 let blob_count: i64 =
112 sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id)
113 .fetch_one(&state.db)
114 .await
115 .unwrap_or(Some(0))
116 .unwrap_or(0);
117
118 let valid_did = did.starts_with("did:");
119
120 (
121 StatusCode::OK,
122 Json(CheckAccountStatusOutput {
123 activated: deactivated_at.is_none(),
124 valid_did,
125 repo_commit: repo_commit.clone(),
126 repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
127 repo_blocks: 0,
128 indexed_records: record_count,
129 private_state_values: 0,
130 expected_blobs: blob_count,
131 imported_blobs: blob_count,
132 }),
133 )
134 .into_response()
135}
136
137pub async fn activate_account(
138 State(state): State<AppState>,
139 headers: axum::http::HeaderMap,
140) -> Response {
141 let auth_header = headers.get("Authorization");
142 if auth_header.is_none() {
143 return (
144 StatusCode::UNAUTHORIZED,
145 Json(json!({"error": "AuthenticationRequired"})),
146 )
147 .into_response();
148 }
149
150 let token = auth_header
151 .unwrap()
152 .to_str()
153 .unwrap_or("")
154 .replace("Bearer ", "");
155
156 let session = sqlx::query!(
157 r#"
158 SELECT s.did, k.key_bytes
159 FROM sessions s
160 JOIN users u ON s.did = u.did
161 JOIN user_keys k ON u.id = k.user_id
162 WHERE s.access_jwt = $1
163 "#,
164 token
165 )
166 .fetch_optional(&state.db)
167 .await;
168
169 let (did, key_bytes) = match session {
170 Ok(Some(row)) => (row.did, row.key_bytes),
171 Ok(None) => {
172 return (
173 StatusCode::UNAUTHORIZED,
174 Json(json!({"error": "AuthenticationFailed"})),
175 )
176 .into_response();
177 }
178 Err(e) => {
179 error!("DB error in activate_account: {:?}", e);
180 return (
181 StatusCode::INTERNAL_SERVER_ERROR,
182 Json(json!({"error": "InternalError"})),
183 )
184 .into_response();
185 }
186 };
187
188 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
189 return (
190 StatusCode::UNAUTHORIZED,
191 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
192 )
193 .into_response();
194 }
195
196 let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
197 .execute(&state.db)
198 .await;
199
200 match result {
201 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
202 Err(e) => {
203 error!("DB error activating account: {:?}", e);
204 (
205 StatusCode::INTERNAL_SERVER_ERROR,
206 Json(json!({"error": "InternalError"})),
207 )
208 .into_response()
209 }
210 }
211}
212
213#[derive(Deserialize)]
214#[serde(rename_all = "camelCase")]
215pub struct DeactivateAccountInput {
216 pub delete_after: Option<String>,
217}
218
219pub async fn deactivate_account(
220 State(state): State<AppState>,
221 headers: axum::http::HeaderMap,
222 Json(_input): Json<DeactivateAccountInput>,
223) -> Response {
224 let auth_header = headers.get("Authorization");
225 if auth_header.is_none() {
226 return (
227 StatusCode::UNAUTHORIZED,
228 Json(json!({"error": "AuthenticationRequired"})),
229 )
230 .into_response();
231 }
232
233 let token = auth_header
234 .unwrap()
235 .to_str()
236 .unwrap_or("")
237 .replace("Bearer ", "");
238
239 let session = sqlx::query!(
240 r#"
241 SELECT s.did, k.key_bytes
242 FROM sessions s
243 JOIN users u ON s.did = u.did
244 JOIN user_keys k ON u.id = k.user_id
245 WHERE s.access_jwt = $1
246 "#,
247 token
248 )
249 .fetch_optional(&state.db)
250 .await;
251
252 let (did, key_bytes) = match session {
253 Ok(Some(row)) => (row.did, row.key_bytes),
254 Ok(None) => {
255 return (
256 StatusCode::UNAUTHORIZED,
257 Json(json!({"error": "AuthenticationFailed"})),
258 )
259 .into_response();
260 }
261 Err(e) => {
262 error!("DB error in deactivate_account: {:?}", e);
263 return (
264 StatusCode::INTERNAL_SERVER_ERROR,
265 Json(json!({"error": "InternalError"})),
266 )
267 .into_response();
268 }
269 };
270
271 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
272 return (
273 StatusCode::UNAUTHORIZED,
274 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
275 )
276 .into_response();
277 }
278
279 let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
280 .execute(&state.db)
281 .await;
282
283 match result {
284 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
285 Err(e) => {
286 error!("DB error deactivating account: {:?}", e);
287 (
288 StatusCode::INTERNAL_SERVER_ERROR,
289 Json(json!({"error": "InternalError"})),
290 )
291 .into_response()
292 }
293 }
294}
295
296pub async fn request_account_delete(
297 State(state): State<AppState>,
298 headers: axum::http::HeaderMap,
299) -> Response {
300 let auth_header = headers.get("Authorization");
301 if auth_header.is_none() {
302 return (
303 StatusCode::UNAUTHORIZED,
304 Json(json!({"error": "AuthenticationRequired"})),
305 )
306 .into_response();
307 }
308
309 let token = auth_header
310 .unwrap()
311 .to_str()
312 .unwrap_or("")
313 .replace("Bearer ", "");
314
315 let session = sqlx::query!(
316 r#"
317 SELECT s.did, u.id as user_id, u.email, u.handle, k.key_bytes
318 FROM sessions s
319 JOIN users u ON s.did = u.did
320 JOIN user_keys k ON u.id = k.user_id
321 WHERE s.access_jwt = $1
322 "#,
323 token
324 )
325 .fetch_optional(&state.db)
326 .await;
327
328 let (did, user_id, email, handle, key_bytes) = match session {
329 Ok(Some(row)) => (row.did, row.user_id, row.email, row.handle, row.key_bytes),
330 Ok(None) => {
331 return (
332 StatusCode::UNAUTHORIZED,
333 Json(json!({"error": "AuthenticationFailed"})),
334 )
335 .into_response();
336 }
337 Err(e) => {
338 error!("DB error in request_account_delete: {:?}", e);
339 return (
340 StatusCode::INTERNAL_SERVER_ERROR,
341 Json(json!({"error": "InternalError"})),
342 )
343 .into_response();
344 }
345 };
346
347 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) {
348 return (
349 StatusCode::UNAUTHORIZED,
350 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})),
351 )
352 .into_response();
353 }
354
355 let confirmation_token = Uuid::new_v4().to_string();
356 let expires_at = Utc::now() + Duration::minutes(15);
357
358 let insert = sqlx::query!(
359 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
360 confirmation_token,
361 did,
362 expires_at
363 )
364 .execute(&state.db)
365 .await;
366
367 if let Err(e) = insert {
368 error!("DB error creating deletion token: {:?}", e);
369 return (
370 StatusCode::INTERNAL_SERVER_ERROR,
371 Json(json!({"error": "InternalError"})),
372 )
373 .into_response();
374 }
375
376 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
377 if let Err(e) = crate::notifications::enqueue_account_deletion(
378 &state.db,
379 user_id,
380 &email,
381 &handle,
382 &confirmation_token,
383 &hostname,
384 )
385 .await
386 {
387 warn!("Failed to enqueue account deletion notification: {:?}", e);
388 }
389
390 info!("Account deletion requested for user {}", did);
391
392 (StatusCode::OK, Json(json!({}))).into_response()
393}