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 result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did)
127 .execute(&state.db)
128 .await;
129
130 match result {
131 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
132 Err(e) => {
133 error!("DB error activating account: {:?}", e);
134 (
135 StatusCode::INTERNAL_SERVER_ERROR,
136 Json(json!({"error": "InternalError"})),
137 )
138 .into_response()
139 }
140 }
141}
142
143#[derive(Deserialize)]
144#[serde(rename_all = "camelCase")]
145pub struct DeactivateAccountInput {
146 pub delete_after: Option<String>,
147}
148
149pub async fn deactivate_account(
150 State(state): State<AppState>,
151 headers: axum::http::HeaderMap,
152 Json(_input): Json<DeactivateAccountInput>,
153) -> Response {
154 let token = match crate::auth::extract_bearer_token_from_header(
155 headers.get("Authorization").and_then(|h| h.to_str().ok())
156 ) {
157 Some(t) => t,
158 None => return ApiError::AuthenticationRequired.into_response(),
159 };
160
161 let did = match crate::auth::validate_bearer_token(&state.db, &token).await {
162 Ok(user) => user.did,
163 Err(e) => return ApiError::from(e).into_response(),
164 };
165
166 let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did)
167 .execute(&state.db)
168 .await;
169
170 match result {
171 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(),
172 Err(e) => {
173 error!("DB error deactivating account: {:?}", e);
174 (
175 StatusCode::INTERNAL_SERVER_ERROR,
176 Json(json!({"error": "InternalError"})),
177 )
178 .into_response()
179 }
180 }
181}
182
183pub async fn request_account_delete(
184 State(state): State<AppState>,
185 headers: axum::http::HeaderMap,
186) -> Response {
187 let token = match crate::auth::extract_bearer_token_from_header(
188 headers.get("Authorization").and_then(|h| h.to_str().ok())
189 ) {
190 Some(t) => t,
191 None => return ApiError::AuthenticationRequired.into_response(),
192 };
193
194 let did = match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &token).await {
195 Ok(user) => user.did,
196 Err(e) => return ApiError::from(e).into_response(),
197 };
198
199 let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did)
200 .fetch_optional(&state.db)
201 .await
202 {
203 Ok(Some(id)) => id,
204 _ => {
205 return (
206 StatusCode::INTERNAL_SERVER_ERROR,
207 Json(json!({"error": "InternalError"})),
208 )
209 .into_response();
210 }
211 };
212
213 let confirmation_token = Uuid::new_v4().to_string();
214 let expires_at = Utc::now() + Duration::minutes(15);
215
216 let insert = sqlx::query!(
217 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)",
218 confirmation_token,
219 did,
220 expires_at
221 )
222 .execute(&state.db)
223 .await;
224
225 if let Err(e) = insert {
226 error!("DB error creating deletion token: {:?}", e);
227 return (
228 StatusCode::INTERNAL_SERVER_ERROR,
229 Json(json!({"error": "InternalError"})),
230 )
231 .into_response();
232 }
233
234 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
235 if let Err(e) =
236 crate::notifications::enqueue_account_deletion(&state.db, user_id, &confirmation_token, &hostname).await
237 {
238 warn!("Failed to enqueue account deletion notification: {:?}", e);
239 }
240
241 info!("Account deletion requested for user {}", did);
242
243 (StatusCode::OK, Json(json!({}))).into_response()
244}
245
246#[derive(Deserialize)]
247pub struct DeleteAccountInput {
248 pub did: String,
249 pub password: String,
250 pub token: String,
251}
252
253pub async fn delete_account(
254 State(state): State<AppState>,
255 Json(input): Json<DeleteAccountInput>,
256) -> Response {
257 let did = input.did.trim();
258 let password = &input.password;
259 let token = input.token.trim();
260
261 if did.is_empty() {
262 return (
263 StatusCode::BAD_REQUEST,
264 Json(json!({"error": "InvalidRequest", "message": "did is required"})),
265 )
266 .into_response();
267 }
268
269 if password.is_empty() {
270 return (
271 StatusCode::BAD_REQUEST,
272 Json(json!({"error": "InvalidRequest", "message": "password is required"})),
273 )
274 .into_response();
275 }
276
277 if token.is_empty() {
278 return (
279 StatusCode::BAD_REQUEST,
280 Json(json!({"error": "InvalidToken", "message": "token is required"})),
281 )
282 .into_response();
283 }
284
285 let user = sqlx::query!(
286 "SELECT id, password_hash FROM users WHERE did = $1",
287 did
288 )
289 .fetch_optional(&state.db)
290 .await;
291
292 let (user_id, password_hash) = match user {
293 Ok(Some(row)) => (row.id, row.password_hash),
294 Ok(None) => {
295 return (
296 StatusCode::BAD_REQUEST,
297 Json(json!({"error": "AccountNotFound", "message": "Account not found"})),
298 )
299 .into_response();
300 }
301 Err(e) => {
302 error!("DB error in delete_account: {:?}", e);
303 return (
304 StatusCode::INTERNAL_SERVER_ERROR,
305 Json(json!({"error": "InternalError"})),
306 )
307 .into_response();
308 }
309 };
310
311 let password_valid = if verify(password, &password_hash).unwrap_or(false) {
312 true
313 } else {
314 let app_pass_rows = sqlx::query!(
315 "SELECT password_hash FROM app_passwords WHERE user_id = $1",
316 user_id
317 )
318 .fetch_all(&state.db)
319 .await
320 .unwrap_or_default();
321
322 app_pass_rows
323 .iter()
324 .any(|row| verify(password, &row.password_hash).unwrap_or(false))
325 };
326
327 if !password_valid {
328 return (
329 StatusCode::UNAUTHORIZED,
330 Json(json!({"error": "AuthenticationFailed", "message": "Invalid password"})),
331 )
332 .into_response();
333 }
334
335 let deletion_request = sqlx::query!(
336 "SELECT did, expires_at FROM account_deletion_requests WHERE token = $1",
337 token
338 )
339 .fetch_optional(&state.db)
340 .await;
341
342 let (token_did, expires_at) = match deletion_request {
343 Ok(Some(row)) => (row.did, row.expires_at),
344 Ok(None) => {
345 return (
346 StatusCode::BAD_REQUEST,
347 Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})),
348 )
349 .into_response();
350 }
351 Err(e) => {
352 error!("DB error fetching deletion token: {:?}", e);
353 return (
354 StatusCode::INTERNAL_SERVER_ERROR,
355 Json(json!({"error": "InternalError"})),
356 )
357 .into_response();
358 }
359 };
360
361 if token_did != did {
362 return (
363 StatusCode::BAD_REQUEST,
364 Json(json!({"error": "InvalidToken", "message": "Token does not match account"})),
365 )
366 .into_response();
367 }
368
369 if Utc::now() > expires_at {
370 let _ = sqlx::query!("DELETE FROM account_deletion_requests WHERE token = $1", token)
371 .execute(&state.db)
372 .await;
373
374 return (
375 StatusCode::BAD_REQUEST,
376 Json(json!({"error": "ExpiredToken", "message": "Token has expired"})),
377 )
378 .into_response();
379 }
380
381 let mut tx = match state.db.begin().await {
382 Ok(tx) => tx,
383 Err(e) => {
384 error!("Failed to begin transaction: {:?}", e);
385 return (
386 StatusCode::INTERNAL_SERVER_ERROR,
387 Json(json!({"error": "InternalError"})),
388 )
389 .into_response();
390 }
391 };
392
393 let deletion_result: Result<(), sqlx::Error> = async {
394 sqlx::query!("DELETE FROM session_tokens WHERE did = $1", did)
395 .execute(&mut *tx)
396 .await?;
397
398 sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id)
399 .execute(&mut *tx)
400 .await?;
401
402 sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id)
403 .execute(&mut *tx)
404 .await?;
405
406 sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id)
407 .execute(&mut *tx)
408 .await?;
409
410 sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id)
411 .execute(&mut *tx)
412 .await?;
413
414 sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1", user_id)
415 .execute(&mut *tx)
416 .await?;
417
418 sqlx::query!("DELETE FROM account_deletion_requests WHERE did = $1", did)
419 .execute(&mut *tx)
420 .await?;
421
422 sqlx::query!("DELETE FROM users WHERE id = $1", user_id)
423 .execute(&mut *tx)
424 .await?;
425
426 Ok(())
427 }
428 .await;
429
430 match deletion_result {
431 Ok(()) => {
432 if let Err(e) = tx.commit().await {
433 error!("Failed to commit account deletion transaction: {:?}", e);
434 return (
435 StatusCode::INTERNAL_SERVER_ERROR,
436 Json(json!({"error": "InternalError"})),
437 )
438 .into_response();
439 }
440 info!("Account {} deleted successfully", did);
441 (StatusCode::OK, Json(json!({}))).into_response()
442 }
443 Err(e) => {
444 error!("DB error deleting account, rolling back: {:?}", e);
445 (
446 StatusCode::INTERNAL_SERVER_ERROR,
447 Json(json!({"error": "InternalError"})),
448 )
449 .into_response()
450 }
451 }
452}