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}