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