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 bcrypt::verify;
9use chrono::{Duration, Utc};
10use serde::{Deserialize, Serialize};
11use serde_json::json;
12use tracing::{error, info, warn};
13use uuid::Uuid;
14
15#[derive(Serialize)]
16#[serde(rename_all = "camelCase")]
17pub struct CheckAccountStatusOutput {
18 pub activated: bool,
19 pub valid_did: bool,
20 pub repo_commit: String,
21 pub repo_rev: String,
22 pub repo_blocks: i64,
23 pub indexed_records: i64,
24 pub private_state_values: i64,
25 pub expected_blobs: i64,
26 pub imported_blobs: i64,
27}
28
29pub async fn check_account_status(
30 State(state): State<AppState>,
31 headers: axum::http::HeaderMap,
32) -> Response {
33 let token = match crate::auth::extract_bearer_token_from_header(
34 headers.get("Authorization").and_then(|h| h.to_str().ok())
35 ) {
36 Some(t) => t,
37 None => {
38 return (
39 StatusCode::UNAUTHORIZED,
40 Json(json!({"error": "AuthenticationRequired"})),
41 )
42 .into_response();
43 }
44 };
45
46 let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
47 let did = match auth_result {
48 Ok(user) => user.did,
49 Err(e) => {
50 return (
51 StatusCode::UNAUTHORIZED,
52 Json(json!({"error": e})),
53 )
54 .into_response();
55 }
56 };
57
58 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
59 .fetch_optional(&state.db)
60 .await
61 {
62 Ok(Some(id)) => id,
63 _ => {
64 return (
65 StatusCode::INTERNAL_SERVER_ERROR,
66 Json(json!({"error": "InternalError"})),
67 )
68 .into_response();
69 }
70 };
71
72 let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did)
73 .fetch_optional(&state.db)
74 .await;
75
76 let deactivated_at = match user_status {
77 Ok(Some(row)) => row.deactivated_at,
78 _ => None,
79 };
80
81 let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id)
82 .fetch_optional(&state.db)
83 .await;
84
85 let repo_commit = match repo_result {
86 Ok(Some(row)) => row.repo_root_cid,
87 _ => String::new(),
88 };
89
90 let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id)
91 .fetch_one(&state.db)
92 .await
93 .unwrap_or(Some(0))
94 .unwrap_or(0);
95
96 let blob_count: i64 =
97 sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id)
98 .fetch_one(&state.db)
99 .await
100 .unwrap_or(Some(0))
101 .unwrap_or(0);
102
103 let valid_did = did.starts_with("did:");
104
105 (
106 StatusCode::OK,
107 Json(CheckAccountStatusOutput {
108 activated: deactivated_at.is_none(),
109 valid_did,
110 repo_commit: repo_commit.clone(),
111 repo_rev: chrono::Utc::now().timestamp_millis().to_string(),
112 repo_blocks: 0,
113 indexed_records: record_count,
114 private_state_values: 0,
115 expected_blobs: blob_count,
116 imported_blobs: blob_count,
117 }),
118 )
119 .into_response()
120}
121
122pub async fn activate_account(
123 State(state): State<AppState>,
124 headers: axum::http::HeaderMap,
125) -> Response {
126 let token = match crate::auth::extract_bearer_token_from_header(
127 headers.get("Authorization").and_then(|h| h.to_str().ok())
128 ) {
129 Some(t) => t,
130 None => {
131 return (
132 StatusCode::UNAUTHORIZED,
133 Json(json!({"error": "AuthenticationRequired"})),
134 )
135 .into_response();
136 }
137 };
138
139 let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
140 let did = match auth_result {
141 Ok(user) => user.did,
142 Err(e) => {
143 return (
144 StatusCode::UNAUTHORIZED,
145 Json(json!({"error": e})),
146 )
147 .into_response();
148 }
149 };
150
151 let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
152 .execute(&state.db)
153 .await;
154
155 match result {
156 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
157 Err(e) => {
158 error!("DB error activating account: {:?}", e);
159 (
160 StatusCode::INTERNAL_SERVER_ERROR,
161 Json(json!({"error": "InternalError"})),
162 )
163 .into_response()
164 }
165 }
166}
167
168#[derive(Deserialize)]
169#[serde(rename_all = "camelCase")]
170pub struct DeactivateAccountInput {
171 pub delete_after: Option<String>,
172}
173
174pub async fn deactivate_account(
175 State(state): State<AppState>,
176 headers: axum::http::HeaderMap,
177 Json(_input): Json<DeactivateAccountInput>,
178) -> Response {
179 let token = match crate::auth::extract_bearer_token_from_header(
180 headers.get("Authorization").and_then(|h| h.to_str().ok())
181 ) {
182 Some(t) => t,
183 None => {
184 return (
185 StatusCode::UNAUTHORIZED,
186 Json(json!({"error": "AuthenticationRequired"})),
187 )
188 .into_response();
189 }
190 };
191
192 let auth_result = crate::auth::validate_bearer_token(&state.db, &token).await;
193 let did = match auth_result {
194 Ok(user) => user.did,
195 Err(e) => {
196 return (
197 StatusCode::UNAUTHORIZED,
198 Json(json!({"error": e})),
199 )
200 .into_response();
201 }
202 };
203
204 let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
205 .execute(&state.db)
206 .await;
207
208 match result {
209 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
210 Err(e) => {
211 error!("DB error deactivating account: {:?}", e);
212 (
213 StatusCode::INTERNAL_SERVER_ERROR,
214 Json(json!({"error": "InternalError"})),
215 )
216 .into_response()
217 }
218 }
219}
220
221pub async fn request_account_delete(
222 State(state): State<AppState>,
223 headers: axum::http::HeaderMap,
224) -> Response {
225 let token = match crate::auth::extract_bearer_token_from_header(
226 headers.get("Authorization").and_then(|h| h.to_str().ok())
227 ) {
228 Some(t) => t,
229 None => {
230 return (
231 StatusCode::UNAUTHORIZED,
232 Json(json!({"error": "AuthenticationRequired"})),
233 )
234 .into_response();
235 }
236 };
237
238 let auth_result = crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await;
239 let did = match auth_result {
240 Ok(user) => user.did,
241 Err(e) => {
242 return (
243 StatusCode::UNAUTHORIZED,
244 Json(json!({"error": e})),
245 )
246 .into_response();
247 }
248 };
249
250 let user = match sqlx::query!("SELECT id, email, handle FROM users WHERE did = $1", did)
251 .fetch_optional(&state.db)
252 .await
253 {
254 Ok(Some(row)) => row,
255 _ => {
256 return (
257 StatusCode::INTERNAL_SERVER_ERROR,
258 Json(json!({"error": "InternalError"})),
259 )
260 .into_response();
261 }
262 };
263 let user_id = user.id;
264 let email = user.email;
265 let handle = user.handle;
266
267 let confirmation_token = Uuid::new_v4().to_string();
268 let expires_at = Utc::now() + Duration::minutes(15);
269
270 let insert = sqlx::query!(
271 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
272 confirmation_token,
273 did,
274 expires_at
275 )
276 .execute(&state.db)
277 .await;
278
279 if let Err(e) = insert {
280 error!("DB error creating deletion token: {:?}", e);
281 return (
282 StatusCode::INTERNAL_SERVER_ERROR,
283 Json(json!({"error": "InternalError"})),
284 )
285 .into_response();
286 }
287
288 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
289 if let Err(e) = crate::notifications::enqueue_account_deletion(
290 &state.db,
291 user_id,
292 &email,
293 &handle,
294 &confirmation_token,
295 &hostname,
296 )
297 .await
298 {
299 warn!("Failed to enqueue account deletion notification: {:?}", e);
300 }
301
302 info!("Account deletion requested for user {}", did);
303
304 (StatusCode::OK, Json(json!({}))).into_response()
305}
306
307#[derive(Deserialize)]
308pub struct DeleteAccountInput {
309 pub did: String,
310 pub password: String,
311 pub token: String,
312}
313
314pub async fn delete_account(
315 State(state): State<AppState>,
316 Json(input): Json<DeleteAccountInput>,
317) -> Response {
318 let did = input.did.trim();
319 let password = &input.password;
320 let token = input.token.trim();
321
322 if did.is_empty() {
323 return (
324 StatusCode::BAD_REQUEST,
325 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
326 )
327 .into_response();
328 }
329
330 if password.is_empty() {
331 return (
332 StatusCode::BAD_REQUEST,
333 Json(json!({"error": "InvalidRequest", "message": "password is required"})),
334 )
335 .into_response();
336 }
337
338 if token.is_empty() {
339 return (
340 StatusCode::BAD_REQUEST,
341 Json(json!({"error": "InvalidToken", "message": "token is required"})),
342 )
343 .into_response();
344 }
345
346 let user = sqlx::query!(
347 "SELECT id, password_hash FROM users WHERE did = $1",
348 did
349 )
350 .fetch_optional(&state.db)
351 .await;
352
353 let (user_id, password_hash) = match user {
354 Ok(Some(row)) => (row.id, row.password_hash),
355 Ok(None) => {
356 return (
357 StatusCode::BAD_REQUEST,
358 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
359 )
360 .into_response();
361 }
362 Err(e) => {
363 error!("DB error in delete_account: {:?}", e);
364 return (
365 StatusCode::INTERNAL_SERVER_ERROR,
366 Json(json!({"error": "InternalError"})),
367 )
368 .into_response();
369 }
370 };
371
372 let password_valid = if verify(password, &password_hash).unwrap_or(false) {
373 true
374 } else {
375 let app_pass_rows = sqlx::query!(
376 "SELECT password_hash FROM app_passwords WHERE user_id = $1",
377 user_id
378 )
379 .fetch_all(&state.db)
380 .await
381 .unwrap_or_default();
382
383 app_pass_rows
384 .iter()
385 .any(|row| verify(password, &row.password_hash).unwrap_or(false))
386 };
387
388 if !password_valid {
389 return (
390 StatusCode::UNAUTHORIZED,
391 Json(json!({"error": "AuthenticationFailed", "message": "Invalid password"})),
392 )
393 .into_response();
394 }
395
396 let deletion_request = sqlx::query!(
397 "SELECT did, expires_at FROM account_deletion_requests WHERE token = $1",
398 token
399 )
400 .fetch_optional(&state.db)
401 .await;
402
403 let (token_did, expires_at) = match deletion_request {
404 Ok(Some(row)) => (row.did, row.expires_at),
405 Ok(None) => {
406 return (
407 StatusCode::BAD_REQUEST,
408 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
409 )
410 .into_response();
411 }
412 Err(e) => {
413 error!("DB error fetching deletion token: {:?}", e);
414 return (
415 StatusCode::INTERNAL_SERVER_ERROR,
416 Json(json!({"error": "InternalError"})),
417 )
418 .into_response();
419 }
420 };
421
422 if token_did != did {
423 return (
424 StatusCode::BAD_REQUEST,
425 Json(json!({"error": "InvalidToken", "message": "Token does not match account"})),
426 )
427 .into_response();
428 }
429
430 if Utc::now() > expires_at {
431 let _ = sqlx::query!("DELETE FROM account_deletion_requests WHERE token = $1", token)
432 .execute(&state.db)
433 .await;
434
435 return (
436 StatusCode::BAD_REQUEST,
437 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
438 )
439 .into_response();
440 }
441
442 let mut tx = match state.db.begin().await {
443 Ok(tx) => tx,
444 Err(e) => {
445 error!("Failed to begin transaction: {:?}", e);
446 return (
447 StatusCode::INTERNAL_SERVER_ERROR,
448 Json(json!({"error": "InternalError"})),
449 )
450 .into_response();
451 }
452 };
453
454 let deletion_result: Result<(), sqlx::Error> = async {
455 sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did)
456 .execute(&mut *tx)
457 .await?;
458
459 sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
460 .execute(&mut *tx)
461 .await?;
462
463 sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
464 .execute(&mut *tx)
465 .await?;
466
467 sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
468 .execute(&mut *tx)
469 .await?;
470
471 sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
472 .execute(&mut *tx)
473 .await?;
474
475 sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id)
476 .execute(&mut *tx)
477 .await?;
478
479 sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did)
480 .execute(&mut *tx)
481 .await?;
482
483 sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
484 .execute(&mut *tx)
485 .await?;
486
487 Ok(())
488 }
489 .await;
490
491 match deletion_result {
492 Ok(()) => {
493 if let Err(e) = tx.commit().await {
494 error!("Failed to commit account deletion transaction: {:?}", e);
495 return (
496 StatusCode::INTERNAL_SERVER_ERROR,
497 Json(json!({"error": "InternalError"})),
498 )
499 .into_response();
500 }
501 info!("Account {} deleted successfully", did);
502 (StatusCode::OK, Json(json!({}))).into_response()
503 }
504 Err(e) => {
505 error!("DB error deleting account, rolling back: {:?}", e);
506 (
507 StatusCode::INTERNAL_SERVER_ERROR,
508 Json(json!({"error": "InternalError"})),
509 )
510 .into_response()
511 }
512 }
513}