this repo has no description
1use crate::state::AppState; 2use axum::{ 3 Json, 4 extract::{Query, State}, 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7}; 8use serde::{Deserialize, Serialize}; 9use serde_json::json; 10use tracing::{error, warn}; 11 12#[derive(Deserialize)] 13pub struct GetAccountInfoParams { 14 pub did: String, 15} 16 17#[derive(Serialize)] 18#[serde(rename_all = "camelCase")] 19pub struct AccountInfo { 20 pub did: String, 21 pub handle: String, 22 pub email: Option<String>, 23 pub indexed_at: String, 24 pub invite_note: Option<String>, 25 pub invites_disabled: bool, 26 pub email_confirmed_at: Option<String>, 27 pub deactivated_at: Option<String>, 28} 29 30#[derive(Serialize)] 31#[serde(rename_all = "camelCase")] 32pub struct GetAccountInfosOutput { 33 pub infos: Vec<AccountInfo>, 34} 35 36pub async fn get_account_info( 37 State(state): State<AppState>, 38 headers: axum::http::HeaderMap, 39 Query(params): Query<GetAccountInfoParams>, 40) -> Response { 41 let auth_header = headers.get("Authorization"); 42 if auth_header.is_none() { 43 return ( 44 StatusCode::UNAUTHORIZED, 45 Json(json!({"error": "AuthenticationRequired"})), 46 ) 47 .into_response(); 48 } 49 50 let did = params.did.trim(); 51 if did.is_empty() { 52 return ( 53 StatusCode::BAD_REQUEST, 54 Json(json!({"error": "InvalidRequest", "message": "did is required"})), 55 ) 56 .into_response(); 57 } 58 59 let result = sqlx::query!( 60 r#" 61 SELECT did, handle, email, created_at 62 FROM users 63 WHERE did = $1 64 "#, 65 did 66 ) 67 .fetch_optional(&state.db) 68 .await; 69 70 match result { 71 Ok(Some(row)) => { 72 ( 73 StatusCode::OK, 74 Json(AccountInfo { 75 did: row.did, 76 handle: row.handle, 77 email: Some(row.email), 78 indexed_at: row.created_at.to_rfc3339(), 79 invite_note: None, 80 invites_disabled: false, 81 email_confirmed_at: None, 82 deactivated_at: None, 83 }), 84 ) 85 .into_response() 86 } 87 Ok(None) => ( 88 StatusCode::NOT_FOUND, 89 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 90 ) 91 .into_response(), 92 Err(e) => { 93 error!("DB error in get_account_info: {:?}", e); 94 ( 95 StatusCode::INTERNAL_SERVER_ERROR, 96 Json(json!({"error": "InternalError"})), 97 ) 98 .into_response() 99 } 100 } 101} 102 103#[derive(Deserialize)] 104pub struct GetAccountInfosParams { 105 pub dids: String, 106} 107 108pub async fn get_account_infos( 109 State(state): State<AppState>, 110 headers: axum::http::HeaderMap, 111 Query(params): Query<GetAccountInfosParams>, 112) -> Response { 113 let auth_header = headers.get("Authorization"); 114 if auth_header.is_none() { 115 return ( 116 StatusCode::UNAUTHORIZED, 117 Json(json!({"error": "AuthenticationRequired"})), 118 ) 119 .into_response(); 120 } 121 122 let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect(); 123 if dids.is_empty() { 124 return ( 125 StatusCode::BAD_REQUEST, 126 Json(json!({"error": "InvalidRequest", "message": "dids is required"})), 127 ) 128 .into_response(); 129 } 130 131 let mut infos = Vec::new(); 132 133 for did in dids { 134 if did.is_empty() { 135 continue; 136 } 137 138 let result = sqlx::query!( 139 r#" 140 SELECT did, handle, email, created_at 141 FROM users 142 WHERE did = $1 143 "#, 144 did 145 ) 146 .fetch_optional(&state.db) 147 .await; 148 149 if let Ok(Some(row)) = result { 150 infos.push(AccountInfo { 151 did: row.did, 152 handle: row.handle, 153 email: Some(row.email), 154 indexed_at: row.created_at.to_rfc3339(), 155 invite_note: None, 156 invites_disabled: false, 157 email_confirmed_at: None, 158 deactivated_at: None, 159 }); 160 } 161 } 162 163 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() 164} 165 166#[derive(Deserialize)] 167pub struct DeleteAccountInput { 168 pub did: String, 169} 170 171pub async fn delete_account( 172 State(state): State<AppState>, 173 headers: axum::http::HeaderMap, 174 Json(input): Json<DeleteAccountInput>, 175) -> Response { 176 let auth_header = headers.get("Authorization"); 177 if auth_header.is_none() { 178 return ( 179 StatusCode::UNAUTHORIZED, 180 Json(json!({"error": "AuthenticationRequired"})), 181 ) 182 .into_response(); 183 } 184 185 let did = input.did.trim(); 186 if did.is_empty() { 187 return ( 188 StatusCode::BAD_REQUEST, 189 Json(json!({"error": "InvalidRequest", "message": "did is required"})), 190 ) 191 .into_response(); 192 } 193 194 let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 195 .fetch_optional(&state.db) 196 .await; 197 198 let user_id = match user { 199 Ok(Some(row)) => row.id, 200 Ok(None) => { 201 return ( 202 StatusCode::NOT_FOUND, 203 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 204 ) 205 .into_response(); 206 } 207 Err(e) => { 208 error!("DB error in delete_account: {:?}", e); 209 return ( 210 StatusCode::INTERNAL_SERVER_ERROR, 211 Json(json!({"error": "InternalError"})), 212 ) 213 .into_response(); 214 } 215 }; 216 217 let _ = sqlx::query!("DELETE FROM sessions WHERE did = $1", did) 218 .execute(&state.db) 219 .await; 220 221 let _ = sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id) 222 .execute(&state.db) 223 .await; 224 225 let _ = sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id) 226 .execute(&state.db) 227 .await; 228 229 let _ = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) 230 .execute(&state.db) 231 .await; 232 233 let _ = sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id) 234 .execute(&state.db) 235 .await; 236 237 let result = sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 238 .execute(&state.db) 239 .await; 240 241 match result { 242 Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 243 Err(e) => { 244 error!("DB error deleting account: {:?}", e); 245 ( 246 StatusCode::INTERNAL_SERVER_ERROR, 247 Json(json!({"error": "InternalError"})), 248 ) 249 .into_response() 250 } 251 } 252} 253 254#[derive(Deserialize)] 255pub struct UpdateAccountEmailInput { 256 pub account: String, 257 pub email: String, 258} 259 260pub async fn update_account_email( 261 State(state): State<AppState>, 262 headers: axum::http::HeaderMap, 263 Json(input): Json<UpdateAccountEmailInput>, 264) -> Response { 265 let auth_header = headers.get("Authorization"); 266 if auth_header.is_none() { 267 return ( 268 StatusCode::UNAUTHORIZED, 269 Json(json!({"error": "AuthenticationRequired"})), 270 ) 271 .into_response(); 272 } 273 274 let account = input.account.trim(); 275 let email = input.email.trim(); 276 277 if account.is_empty() || email.is_empty() { 278 return ( 279 StatusCode::BAD_REQUEST, 280 Json(json!({"error": "InvalidRequest", "message": "account and email are required"})), 281 ) 282 .into_response(); 283 } 284 285 let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account) 286 .execute(&state.db) 287 .await; 288 289 match result { 290 Ok(r) => { 291 if r.rows_affected() == 0 { 292 return ( 293 StatusCode::NOT_FOUND, 294 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 295 ) 296 .into_response(); 297 } 298 (StatusCode::OK, Json(json!({}))).into_response() 299 } 300 Err(e) => { 301 error!("DB error updating email: {:?}", e); 302 ( 303 StatusCode::INTERNAL_SERVER_ERROR, 304 Json(json!({"error": "InternalError"})), 305 ) 306 .into_response() 307 } 308 } 309} 310 311#[derive(Deserialize)] 312pub struct UpdateAccountHandleInput { 313 pub did: String, 314 pub handle: String, 315} 316 317pub async fn update_account_handle( 318 State(state): State<AppState>, 319 headers: axum::http::HeaderMap, 320 Json(input): Json<UpdateAccountHandleInput>, 321) -> Response { 322 let auth_header = headers.get("Authorization"); 323 if auth_header.is_none() { 324 return ( 325 StatusCode::UNAUTHORIZED, 326 Json(json!({"error": "AuthenticationRequired"})), 327 ) 328 .into_response(); 329 } 330 331 let did = input.did.trim(); 332 let handle = input.handle.trim(); 333 334 if did.is_empty() || handle.is_empty() { 335 return ( 336 StatusCode::BAD_REQUEST, 337 Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})), 338 ) 339 .into_response(); 340 } 341 342 if !handle 343 .chars() 344 .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 345 { 346 return ( 347 StatusCode::BAD_REQUEST, 348 Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})), 349 ) 350 .into_response(); 351 } 352 353 let existing = sqlx::query!("SELECT id FROM users WHERE handle = $1 AND did != $2", handle, did) 354 .fetch_optional(&state.db) 355 .await; 356 357 if let Ok(Some(_)) = existing { 358 return ( 359 StatusCode::BAD_REQUEST, 360 Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})), 361 ) 362 .into_response(); 363 } 364 365 let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did) 366 .execute(&state.db) 367 .await; 368 369 match result { 370 Ok(r) => { 371 if r.rows_affected() == 0 { 372 return ( 373 StatusCode::NOT_FOUND, 374 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 375 ) 376 .into_response(); 377 } 378 (StatusCode::OK, Json(json!({}))).into_response() 379 } 380 Err(e) => { 381 error!("DB error updating handle: {:?}", e); 382 ( 383 StatusCode::INTERNAL_SERVER_ERROR, 384 Json(json!({"error": "InternalError"})), 385 ) 386 .into_response() 387 } 388 } 389} 390 391#[derive(Deserialize)] 392pub struct UpdateAccountPasswordInput { 393 pub did: String, 394 pub password: String, 395} 396 397pub async fn update_account_password( 398 State(state): State<AppState>, 399 headers: axum::http::HeaderMap, 400 Json(input): Json<UpdateAccountPasswordInput>, 401) -> Response { 402 let auth_header = headers.get("Authorization"); 403 if auth_header.is_none() { 404 return ( 405 StatusCode::UNAUTHORIZED, 406 Json(json!({"error": "AuthenticationRequired"})), 407 ) 408 .into_response(); 409 } 410 411 let did = input.did.trim(); 412 let password = input.password.trim(); 413 414 if did.is_empty() || password.is_empty() { 415 return ( 416 StatusCode::BAD_REQUEST, 417 Json(json!({"error": "InvalidRequest", "message": "did and password are required"})), 418 ) 419 .into_response(); 420 } 421 422 let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) { 423 Ok(h) => h, 424 Err(e) => { 425 error!("Failed to hash password: {:?}", e); 426 return ( 427 StatusCode::INTERNAL_SERVER_ERROR, 428 Json(json!({"error": "InternalError"})), 429 ) 430 .into_response(); 431 } 432 }; 433 434 let result = sqlx::query!("UPDATE users SET password_hash = $1 WHERE did = $2", password_hash, did) 435 .execute(&state.db) 436 .await; 437 438 match result { 439 Ok(r) => { 440 if r.rows_affected() == 0 { 441 return ( 442 StatusCode::NOT_FOUND, 443 Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 444 ) 445 .into_response(); 446 } 447 (StatusCode::OK, Json(json!({}))).into_response() 448 } 449 Err(e) => { 450 error!("DB error updating password: {:?}", e); 451 ( 452 StatusCode::INTERNAL_SERVER_ERROR, 453 Json(json!({"error": "InternalError"})), 454 ) 455 .into_response() 456 } 457 } 458} 459 460#[derive(Deserialize)] 461#[serde(rename_all = "camelCase")] 462pub struct SendEmailInput { 463 pub recipient_did: String, 464 pub sender_did: String, 465 pub content: String, 466 pub subject: Option<String>, 467 pub comment: Option<String>, 468} 469 470#[derive(Serialize)] 471pub struct SendEmailOutput { 472 pub sent: bool, 473} 474 475pub async fn send_email( 476 State(state): State<AppState>, 477 headers: axum::http::HeaderMap, 478 Json(input): Json<SendEmailInput>, 479) -> Response { 480 let auth_header = headers.get("Authorization"); 481 if auth_header.is_none() { 482 return ( 483 StatusCode::UNAUTHORIZED, 484 Json(json!({"error": "AuthenticationRequired"})), 485 ) 486 .into_response(); 487 } 488 489 let recipient_did = input.recipient_did.trim(); 490 let content = input.content.trim(); 491 492 if recipient_did.is_empty() { 493 return ( 494 StatusCode::BAD_REQUEST, 495 Json(json!({"error": "InvalidRequest", "message": "recipientDid is required"})), 496 ) 497 .into_response(); 498 } 499 500 if content.is_empty() { 501 return ( 502 StatusCode::BAD_REQUEST, 503 Json(json!({"error": "InvalidRequest", "message": "content is required"})), 504 ) 505 .into_response(); 506 } 507 508 let user = sqlx::query!( 509 "SELECT id, email, handle FROM users WHERE did = $1", 510 recipient_did 511 ) 512 .fetch_optional(&state.db) 513 .await; 514 515 let (user_id, email, handle) = match user { 516 Ok(Some(row)) => (row.id, row.email, row.handle), 517 Ok(None) => { 518 return ( 519 StatusCode::NOT_FOUND, 520 Json(json!({"error": "AccountNotFound", "message": "Recipient account not found"})), 521 ) 522 .into_response(); 523 } 524 Err(e) => { 525 error!("DB error in send_email: {:?}", e); 526 return ( 527 StatusCode::INTERNAL_SERVER_ERROR, 528 Json(json!({"error": "InternalError"})), 529 ) 530 .into_response(); 531 } 532 }; 533 534 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 535 let subject = input 536 .subject 537 .clone() 538 .unwrap_or_else(|| format!("Message from {}", hostname)); 539 540 let notification = crate::notifications::NewNotification::email( 541 user_id, 542 crate::notifications::NotificationType::AdminEmail, 543 email, 544 subject, 545 content.to_string(), 546 ); 547 548 let result = crate::notifications::enqueue_notification(&state.db, notification).await; 549 550 match result { 551 Ok(_) => { 552 tracing::info!( 553 "Admin email queued for {} ({})", 554 handle, 555 recipient_did 556 ); 557 (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 558 } 559 Err(e) => { 560 warn!("Failed to enqueue admin email: {:?}", e); 561 (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() 562 } 563 } 564}