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