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}