this repo has no description
1use crate::state::AppState; 2use axum::{ 3 Json, 4 extract::State, 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7}; 8use chrono::{Duration, Utc}; 9use serde::{Deserialize, Serialize}; 10use serde_json::json; 11use tracing::{error, info, warn}; 12use uuid::Uuid; 13 14#[derive(Serialize)] 15#[serde(rename_all = "camelCase")] 16pub struct CheckAccountStatusOutput { 17 pub activated: bool, 18 pub valid_did: bool, 19 pub repo_commit: String, 20 pub repo_rev: String, 21 pub repo_blocks: i64, 22 pub indexed_records: i64, 23 pub private_state_values: i64, 24 pub expected_blobs: i64, 25 pub imported_blobs: i64, 26} 27 28pub async fn check_account_status( 29 State(state): State<AppState>, 30 headers: axum::http::HeaderMap, 31) -> Response { 32 let auth_header = headers.get("Authorization"); 33 if auth_header.is_none() { 34 return ( 35 StatusCode::UNAUTHORIZED, 36 Json(json!({"error": "AuthenticationRequired"})), 37 ) 38 .into_response(); 39 } 40 41 let token = auth_header 42 .unwrap() 43 .to_str() 44 .unwrap_or("") 45 .replace("Bearer ", ""); 46 47 let session = sqlx::query!( 48 r#" 49 SELECT s.did, k.key_bytes, u.id as user_id 50 FROM sessions s 51 JOIN users u ON s.did = u.did 52 JOIN user_keys k ON u.id = k.user_id 53 WHERE s.access_jwt = $1 54 "#, 55 token 56 ) 57 .fetch_optional(&state.db) 58 .await; 59 60 let (did, key_bytes, user_id) = match session { 61 Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 62 Ok(None) => { 63 return ( 64 StatusCode::UNAUTHORIZED, 65 Json(json!({"error": "AuthenticationFailed"})), 66 ) 67 .into_response(); 68 } 69 Err(e) => { 70 error!("DB error in check_account_status: {:?}", e); 71 return ( 72 StatusCode::INTERNAL_SERVER_ERROR, 73 Json(json!({"error": "InternalError"})), 74 ) 75 .into_response(); 76 } 77 }; 78 79 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 80 return ( 81 StatusCode::UNAUTHORIZED, 82 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 83 ) 84 .into_response(); 85 } 86 87 let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did) 88 .fetch_optional(&state.db) 89 .await; 90 91 let deactivated_at = match user_status { 92 Ok(Some(row)) => row.deactivated_at, 93 _ => None, 94 }; 95 96 let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id) 97 .fetch_optional(&state.db) 98 .await; 99 100 let repo_commit = match repo_result { 101 Ok(Some(row)) => row.repo_root_cid, 102 _ => String::new(), 103 }; 104 105 let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id) 106 .fetch_one(&state.db) 107 .await 108 .unwrap_or(Some(0)) 109 .unwrap_or(0); 110 111 let blob_count: i64 = 112 sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id) 113 .fetch_one(&state.db) 114 .await 115 .unwrap_or(Some(0)) 116 .unwrap_or(0); 117 118 let valid_did = did.starts_with("did:"); 119 120 ( 121 StatusCode::OK, 122 Json(CheckAccountStatusOutput { 123 activated: deactivated_at.is_none(), 124 valid_did, 125 repo_commit: repo_commit.clone(), 126 repo_rev: chrono::Utc::now().timestamp_millis().to_string(), 127 repo_blocks: 0, 128 indexed_records: record_count, 129 private_state_values: 0, 130 expected_blobs: blob_count, 131 imported_blobs: blob_count, 132 }), 133 ) 134 .into_response() 135} 136 137pub async fn activate_account( 138 State(state): State<AppState>, 139 headers: axum::http::HeaderMap, 140) -> Response { 141 let auth_header = headers.get("Authorization"); 142 if auth_header.is_none() { 143 return ( 144 StatusCode::UNAUTHORIZED, 145 Json(json!({"error": "AuthenticationRequired"})), 146 ) 147 .into_response(); 148 } 149 150 let token = auth_header 151 .unwrap() 152 .to_str() 153 .unwrap_or("") 154 .replace("Bearer ", ""); 155 156 let session = sqlx::query!( 157 r#" 158 SELECT s.did, k.key_bytes 159 FROM sessions s 160 JOIN users u ON s.did = u.did 161 JOIN user_keys k ON u.id = k.user_id 162 WHERE s.access_jwt = $1 163 "#, 164 token 165 ) 166 .fetch_optional(&state.db) 167 .await; 168 169 let (did, key_bytes) = match session { 170 Ok(Some(row)) => (row.did, row.key_bytes), 171 Ok(None) => { 172 return ( 173 StatusCode::UNAUTHORIZED, 174 Json(json!({"error": "AuthenticationFailed"})), 175 ) 176 .into_response(); 177 } 178 Err(e) => { 179 error!("DB error in activate_account: {:?}", e); 180 return ( 181 StatusCode::INTERNAL_SERVER_ERROR, 182 Json(json!({"error": "InternalError"})), 183 ) 184 .into_response(); 185 } 186 }; 187 188 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 189 return ( 190 StatusCode::UNAUTHORIZED, 191 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 192 ) 193 .into_response(); 194 } 195 196 let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did) 197 .execute(&state.db) 198 .await; 199 200 match result { 201 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 202 Err(e) => { 203 error!("DB error activating account: {:?}", e); 204 ( 205 StatusCode::INTERNAL_SERVER_ERROR, 206 Json(json!({"error": "InternalError"})), 207 ) 208 .into_response() 209 } 210 } 211} 212 213#[derive(Deserialize)] 214#[serde(rename_all = "camelCase")] 215pub struct DeactivateAccountInput { 216 pub delete_after: Option<String>, 217} 218 219pub async fn deactivate_account( 220 State(state): State<AppState>, 221 headers: axum::http::HeaderMap, 222 Json(_input): Json<DeactivateAccountInput>, 223) -> Response { 224 let auth_header = headers.get("Authorization"); 225 if auth_header.is_none() { 226 return ( 227 StatusCode::UNAUTHORIZED, 228 Json(json!({"error": "AuthenticationRequired"})), 229 ) 230 .into_response(); 231 } 232 233 let token = auth_header 234 .unwrap() 235 .to_str() 236 .unwrap_or("") 237 .replace("Bearer ", ""); 238 239 let session = sqlx::query!( 240 r#" 241 SELECT s.did, k.key_bytes 242 FROM sessions s 243 JOIN users u ON s.did = u.did 244 JOIN user_keys k ON u.id = k.user_id 245 WHERE s.access_jwt = $1 246 "#, 247 token 248 ) 249 .fetch_optional(&state.db) 250 .await; 251 252 let (did, key_bytes) = match session { 253 Ok(Some(row)) => (row.did, row.key_bytes), 254 Ok(None) => { 255 return ( 256 StatusCode::UNAUTHORIZED, 257 Json(json!({"error": "AuthenticationFailed"})), 258 ) 259 .into_response(); 260 } 261 Err(e) => { 262 error!("DB error in deactivate_account: {:?}", e); 263 return ( 264 StatusCode::INTERNAL_SERVER_ERROR, 265 Json(json!({"error": "InternalError"})), 266 ) 267 .into_response(); 268 } 269 }; 270 271 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 272 return ( 273 StatusCode::UNAUTHORIZED, 274 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 275 ) 276 .into_response(); 277 } 278 279 let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did) 280 .execute(&state.db) 281 .await; 282 283 match result { 284 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 285 Err(e) => { 286 error!("DB error deactivating account: {:?}", e); 287 ( 288 StatusCode::INTERNAL_SERVER_ERROR, 289 Json(json!({"error": "InternalError"})), 290 ) 291 .into_response() 292 } 293 } 294} 295 296pub async fn request_account_delete( 297 State(state): State<AppState>, 298 headers: axum::http::HeaderMap, 299) -> Response { 300 let auth_header = headers.get("Authorization"); 301 if auth_header.is_none() { 302 return ( 303 StatusCode::UNAUTHORIZED, 304 Json(json!({"error": "AuthenticationRequired"})), 305 ) 306 .into_response(); 307 } 308 309 let token = auth_header 310 .unwrap() 311 .to_str() 312 .unwrap_or("") 313 .replace("Bearer ", ""); 314 315 let session = sqlx::query!( 316 r#" 317 SELECT s.did, u.id as user_id, u.email, u.handle, k.key_bytes 318 FROM sessions s 319 JOIN users u ON s.did = u.did 320 JOIN user_keys k ON u.id = k.user_id 321 WHERE s.access_jwt = $1 322 "#, 323 token 324 ) 325 .fetch_optional(&state.db) 326 .await; 327 328 let (did, user_id, email, handle, key_bytes) = match session { 329 Ok(Some(row)) => (row.did, row.user_id, row.email, row.handle, row.key_bytes), 330 Ok(None) => { 331 return ( 332 StatusCode::UNAUTHORIZED, 333 Json(json!({"error": "AuthenticationFailed"})), 334 ) 335 .into_response(); 336 } 337 Err(e) => { 338 error!("DB error in request_account_delete: {:?}", e); 339 return ( 340 StatusCode::INTERNAL_SERVER_ERROR, 341 Json(json!({"error": "InternalError"})), 342 ) 343 .into_response(); 344 } 345 }; 346 347 if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 348 return ( 349 StatusCode::UNAUTHORIZED, 350 Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 351 ) 352 .into_response(); 353 } 354 355 let confirmation_token = Uuid::new_v4().to_string(); 356 let expires_at = Utc::now() + Duration::minutes(15); 357 358 let insert = sqlx::query!( 359 "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)", 360 confirmation_token, 361 did, 362 expires_at 363 ) 364 .execute(&state.db) 365 .await; 366 367 if let Err(e) = insert { 368 error!("DB error creating deletion token: {:?}", e); 369 return ( 370 StatusCode::INTERNAL_SERVER_ERROR, 371 Json(json!({"error": "InternalError"})), 372 ) 373 .into_response(); 374 } 375 376 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 377 if let Err(e) = crate::notifications::enqueue_account_deletion( 378 &state.db, 379 user_id, 380 &email, 381 &handle, 382 &confirmation_token, 383 &hostname, 384 ) 385 .await 386 { 387 warn!("Failed to enqueue account deletion notification: {:?}", e); 388 } 389 390 info!("Account deletion requested for user {}", did); 391 392 (StatusCode::OK, Json(json!({}))).into_response() 393}