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}