this repo has no description

Files were getting too large, logically split them

lewis 1fe38f49 9f78f91d

+564
src/api/admin/account.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Query, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use serde::{Deserialize, Serialize}; 9 + use serde_json::json; 10 + use tracing::{error, warn}; 11 + 12 + #[derive(Deserialize)] 13 + pub struct GetAccountInfoParams { 14 + pub did: String, 15 + } 16 + 17 + #[derive(Serialize)] 18 + #[serde(rename_all = "camelCase")] 19 + pub 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")] 32 + pub struct GetAccountInfosOutput { 33 + pub infos: Vec<AccountInfo>, 34 + } 35 + 36 + pub 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)] 104 + pub struct GetAccountInfosParams { 105 + pub dids: String, 106 + } 107 + 108 + pub 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)] 167 + pub struct DeleteAccountInput { 168 + pub did: String, 169 + } 170 + 171 + pub 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)] 255 + pub struct UpdateAccountEmailInput { 256 + pub account: String, 257 + pub email: String, 258 + } 259 + 260 + pub 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)] 312 + pub struct UpdateAccountHandleInput { 313 + pub did: String, 314 + pub handle: String, 315 + } 316 + 317 + pub 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)] 392 + pub struct UpdateAccountPasswordInput { 393 + pub did: String, 394 + pub password: String, 395 + } 396 + 397 + pub 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")] 462 + pub 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)] 471 + pub struct SendEmailOutput { 472 + pub sent: bool, 473 + } 474 + 475 + pub 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 + }
+323
src/api/admin/invite.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Query, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use serde::{Deserialize, Serialize}; 9 + use serde_json::json; 10 + use tracing::error; 11 + 12 + #[derive(Deserialize)] 13 + #[serde(rename_all = "camelCase")] 14 + pub struct DisableInviteCodesInput { 15 + pub codes: Option<Vec<String>>, 16 + pub accounts: Option<Vec<String>>, 17 + } 18 + 19 + pub async fn disable_invite_codes( 20 + State(state): State<AppState>, 21 + headers: axum::http::HeaderMap, 22 + Json(input): Json<DisableInviteCodesInput>, 23 + ) -> Response { 24 + let auth_header = headers.get("Authorization"); 25 + if auth_header.is_none() { 26 + return ( 27 + StatusCode::UNAUTHORIZED, 28 + Json(json!({"error": "AuthenticationRequired"})), 29 + ) 30 + .into_response(); 31 + } 32 + 33 + if let Some(codes) = &input.codes { 34 + for code in codes { 35 + let _ = sqlx::query!("UPDATE invite_codes SET disabled = TRUE WHERE code = $1", code) 36 + .execute(&state.db) 37 + .await; 38 + } 39 + } 40 + 41 + if let Some(accounts) = &input.accounts { 42 + for account in accounts { 43 + let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account) 44 + .fetch_optional(&state.db) 45 + .await; 46 + 47 + if let Ok(Some(user_row)) = user { 48 + let _ = sqlx::query!( 49 + "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1", 50 + user_row.id 51 + ) 52 + .execute(&state.db) 53 + .await; 54 + } 55 + } 56 + } 57 + 58 + (StatusCode::OK, Json(json!({}))).into_response() 59 + } 60 + 61 + #[derive(Deserialize)] 62 + pub struct GetInviteCodesParams { 63 + pub sort: Option<String>, 64 + pub limit: Option<i64>, 65 + pub cursor: Option<String>, 66 + } 67 + 68 + #[derive(Serialize)] 69 + #[serde(rename_all = "camelCase")] 70 + pub struct InviteCodeInfo { 71 + pub code: String, 72 + pub available: i32, 73 + pub disabled: bool, 74 + pub for_account: String, 75 + pub created_by: String, 76 + pub created_at: String, 77 + pub uses: Vec<InviteCodeUseInfo>, 78 + } 79 + 80 + #[derive(Serialize)] 81 + #[serde(rename_all = "camelCase")] 82 + pub struct InviteCodeUseInfo { 83 + pub used_by: String, 84 + pub used_at: String, 85 + } 86 + 87 + #[derive(Serialize)] 88 + pub struct GetInviteCodesOutput { 89 + pub cursor: Option<String>, 90 + pub codes: Vec<InviteCodeInfo>, 91 + } 92 + 93 + pub async fn get_invite_codes( 94 + State(state): State<AppState>, 95 + headers: axum::http::HeaderMap, 96 + Query(params): Query<GetInviteCodesParams>, 97 + ) -> Response { 98 + let auth_header = headers.get("Authorization"); 99 + if auth_header.is_none() { 100 + return ( 101 + StatusCode::UNAUTHORIZED, 102 + Json(json!({"error": "AuthenticationRequired"})), 103 + ) 104 + .into_response(); 105 + } 106 + 107 + let limit = params.limit.unwrap_or(100).min(500); 108 + let sort = params.sort.as_deref().unwrap_or("recent"); 109 + 110 + let order_clause = match sort { 111 + "usage" => "available_uses DESC", 112 + _ => "created_at DESC", 113 + }; 114 + 115 + let codes_result = if let Some(cursor) = &params.cursor { 116 + sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!( 117 + r#" 118 + SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 119 + FROM invite_codes ic 120 + WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 121 + ORDER BY {} 122 + LIMIT $2 123 + "#, 124 + order_clause 125 + )) 126 + .bind(cursor) 127 + .bind(limit) 128 + .fetch_all(&state.db) 129 + .await 130 + } else { 131 + sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!( 132 + r#" 133 + SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 134 + FROM invite_codes ic 135 + ORDER BY {} 136 + LIMIT $1 137 + "#, 138 + order_clause 139 + )) 140 + .bind(limit) 141 + .fetch_all(&state.db) 142 + .await 143 + }; 144 + 145 + let codes_rows = match codes_result { 146 + Ok(rows) => rows, 147 + Err(e) => { 148 + error!("DB error fetching invite codes: {:?}", e); 149 + return ( 150 + StatusCode::INTERNAL_SERVER_ERROR, 151 + Json(json!({"error": "InternalError"})), 152 + ) 153 + .into_response(); 154 + } 155 + }; 156 + 157 + let mut codes = Vec::new(); 158 + for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows { 159 + let creator_did = sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user) 160 + .fetch_optional(&state.db) 161 + .await 162 + .ok() 163 + .flatten() 164 + .unwrap_or_else(|| "unknown".to_string()); 165 + 166 + let uses_result = sqlx::query!( 167 + r#" 168 + SELECT u.did, icu.used_at 169 + FROM invite_code_uses icu 170 + JOIN users u ON icu.used_by_user = u.id 171 + WHERE icu.code = $1 172 + ORDER BY icu.used_at DESC 173 + "#, 174 + code 175 + ) 176 + .fetch_all(&state.db) 177 + .await; 178 + 179 + let uses = match uses_result { 180 + Ok(use_rows) => use_rows 181 + .iter() 182 + .map(|u| InviteCodeUseInfo { 183 + used_by: u.did.clone(), 184 + used_at: u.used_at.to_rfc3339(), 185 + }) 186 + .collect(), 187 + Err(_) => Vec::new(), 188 + }; 189 + 190 + codes.push(InviteCodeInfo { 191 + code: code.clone(), 192 + available: *available_uses, 193 + disabled: disabled.unwrap_or(false), 194 + for_account: creator_did.clone(), 195 + created_by: creator_did, 196 + created_at: created_at.to_rfc3339(), 197 + uses, 198 + }); 199 + } 200 + 201 + let next_cursor = if codes_rows.len() == limit as usize { 202 + codes_rows.last().map(|(code, _, _, _, _)| code.clone()) 203 + } else { 204 + None 205 + }; 206 + 207 + ( 208 + StatusCode::OK, 209 + Json(GetInviteCodesOutput { 210 + cursor: next_cursor, 211 + codes, 212 + }), 213 + ) 214 + .into_response() 215 + } 216 + 217 + #[derive(Deserialize)] 218 + pub struct DisableAccountInvitesInput { 219 + pub account: String, 220 + } 221 + 222 + pub async fn disable_account_invites( 223 + State(state): State<AppState>, 224 + headers: axum::http::HeaderMap, 225 + Json(input): Json<DisableAccountInvitesInput>, 226 + ) -> Response { 227 + let auth_header = headers.get("Authorization"); 228 + if auth_header.is_none() { 229 + return ( 230 + StatusCode::UNAUTHORIZED, 231 + Json(json!({"error": "AuthenticationRequired"})), 232 + ) 233 + .into_response(); 234 + } 235 + 236 + let account = input.account.trim(); 237 + if account.is_empty() { 238 + return ( 239 + StatusCode::BAD_REQUEST, 240 + Json(json!({"error": "InvalidRequest", "message": "account is required"})), 241 + ) 242 + .into_response(); 243 + } 244 + 245 + let result = sqlx::query!("UPDATE users SET invites_disabled = TRUE WHERE did = $1", account) 246 + .execute(&state.db) 247 + .await; 248 + 249 + match result { 250 + Ok(r) => { 251 + if r.rows_affected() == 0 { 252 + return ( 253 + StatusCode::NOT_FOUND, 254 + Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 255 + ) 256 + .into_response(); 257 + } 258 + (StatusCode::OK, Json(json!({}))).into_response() 259 + } 260 + Err(e) => { 261 + error!("DB error disabling account invites: {:?}", e); 262 + ( 263 + StatusCode::INTERNAL_SERVER_ERROR, 264 + Json(json!({"error": "InternalError"})), 265 + ) 266 + .into_response() 267 + } 268 + } 269 + } 270 + 271 + #[derive(Deserialize)] 272 + pub struct EnableAccountInvitesInput { 273 + pub account: String, 274 + } 275 + 276 + pub async fn enable_account_invites( 277 + State(state): State<AppState>, 278 + headers: axum::http::HeaderMap, 279 + Json(input): Json<EnableAccountInvitesInput>, 280 + ) -> Response { 281 + let auth_header = headers.get("Authorization"); 282 + if auth_header.is_none() { 283 + return ( 284 + StatusCode::UNAUTHORIZED, 285 + Json(json!({"error": "AuthenticationRequired"})), 286 + ) 287 + .into_response(); 288 + } 289 + 290 + let account = input.account.trim(); 291 + if account.is_empty() { 292 + return ( 293 + StatusCode::BAD_REQUEST, 294 + Json(json!({"error": "InvalidRequest", "message": "account is required"})), 295 + ) 296 + .into_response(); 297 + } 298 + 299 + let result = sqlx::query!("UPDATE users SET invites_disabled = FALSE WHERE did = $1", account) 300 + .execute(&state.db) 301 + .await; 302 + 303 + match result { 304 + Ok(r) => { 305 + if r.rows_affected() == 0 { 306 + return ( 307 + StatusCode::NOT_FOUND, 308 + Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 309 + ) 310 + .into_response(); 311 + } 312 + (StatusCode::OK, Json(json!({}))).into_response() 313 + } 314 + Err(e) => { 315 + error!("DB error enabling account invites: {:?}", e); 316 + ( 317 + StatusCode::INTERNAL_SERVER_ERROR, 318 + Json(json!({"error": "InternalError"})), 319 + ) 320 + .into_response() 321 + } 322 + } 323 + }
+11 -1222
src/api/admin/mod.rs
··· 1 - use crate::state::AppState; 2 - use axum::{ 3 - Json, 4 - extract::{Query, State}, 5 - http::StatusCode, 6 - response::{IntoResponse, Response}, 7 - }; 8 - use serde::{Deserialize, Serialize}; 9 - use serde_json::json; 10 - use tracing::{error, warn}; 11 - 12 - #[derive(Deserialize)] 13 - #[serde(rename_all = "camelCase")] 14 - pub struct DisableInviteCodesInput { 15 - pub codes: Option<Vec<String>>, 16 - pub accounts: Option<Vec<String>>, 17 - } 18 - 19 - pub async fn disable_invite_codes( 20 - State(state): State<AppState>, 21 - headers: axum::http::HeaderMap, 22 - Json(input): Json<DisableInviteCodesInput>, 23 - ) -> Response { 24 - let auth_header = headers.get("Authorization"); 25 - if auth_header.is_none() { 26 - return ( 27 - StatusCode::UNAUTHORIZED, 28 - Json(json!({"error": "AuthenticationRequired"})), 29 - ) 30 - .into_response(); 31 - } 32 - 33 - if let Some(codes) = &input.codes { 34 - for code in codes { 35 - let _ = sqlx::query!("UPDATE invite_codes SET disabled = TRUE WHERE code = $1", code) 36 - .execute(&state.db) 37 - .await; 38 - } 39 - } 40 - 41 - if let Some(accounts) = &input.accounts { 42 - for account in accounts { 43 - let user = sqlx::query!("SELECT id FROM users WHERE did = $1", account) 44 - .fetch_optional(&state.db) 45 - .await; 46 47 - if let Ok(Some(user_row)) = user { 48 - let _ = sqlx::query!( 49 - "UPDATE invite_codes SET disabled = TRUE WHERE created_by_user = $1", 50 - user_row.id 51 - ) 52 - .execute(&state.db) 53 - .await; 54 - } 55 - } 56 - } 57 - 58 - (StatusCode::OK, Json(json!({}))).into_response() 59 - } 60 - 61 - #[derive(Deserialize)] 62 - pub struct GetSubjectStatusParams { 63 - pub did: Option<String>, 64 - pub uri: Option<String>, 65 - pub blob: Option<String>, 66 - } 67 - 68 - #[derive(Serialize)] 69 - pub struct SubjectStatus { 70 - pub subject: serde_json::Value, 71 - pub takedown: Option<StatusAttr>, 72 - pub deactivated: Option<StatusAttr>, 73 - } 74 - 75 - #[derive(Serialize)] 76 - #[serde(rename_all = "camelCase")] 77 - pub struct StatusAttr { 78 - pub applied: bool, 79 - pub r#ref: Option<String>, 80 - } 81 - 82 - pub async fn get_subject_status( 83 - State(state): State<AppState>, 84 - headers: axum::http::HeaderMap, 85 - Query(params): Query<GetSubjectStatusParams>, 86 - ) -> Response { 87 - let auth_header = headers.get("Authorization"); 88 - if auth_header.is_none() { 89 - return ( 90 - StatusCode::UNAUTHORIZED, 91 - Json(json!({"error": "AuthenticationRequired"})), 92 - ) 93 - .into_response(); 94 - } 95 - 96 - if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 97 - return ( 98 - StatusCode::BAD_REQUEST, 99 - Json(json!({"error": "InvalidRequest", "message": "Must provide did, uri, or blob"})), 100 - ) 101 - .into_response(); 102 - } 103 - 104 - if let Some(did) = &params.did { 105 - let user = sqlx::query!( 106 - "SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1", 107 - did 108 - ) 109 - .fetch_optional(&state.db) 110 - .await; 111 - 112 - match user { 113 - Ok(Some(row)) => { 114 - let deactivated = row.deactivated_at.map(|_| StatusAttr { 115 - applied: true, 116 - r#ref: None, 117 - }); 118 - let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr { 119 - applied: true, 120 - r#ref: Some(r.clone()), 121 - }); 122 - 123 - return ( 124 - StatusCode::OK, 125 - Json(SubjectStatus { 126 - subject: json!({ 127 - "$type": "com.atproto.admin.defs#repoRef", 128 - "did": row.did 129 - }), 130 - takedown, 131 - deactivated, 132 - }), 133 - ) 134 - .into_response(); 135 - } 136 - Ok(None) => { 137 - return ( 138 - StatusCode::NOT_FOUND, 139 - Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 140 - ) 141 - .into_response(); 142 - } 143 - Err(e) => { 144 - error!("DB error in get_subject_status: {:?}", e); 145 - return ( 146 - StatusCode::INTERNAL_SERVER_ERROR, 147 - Json(json!({"error": "InternalError"})), 148 - ) 149 - .into_response(); 150 - } 151 - } 152 - } 153 - 154 - if let Some(uri) = &params.uri { 155 - let record = sqlx::query!( 156 - "SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1", 157 - uri 158 - ) 159 - .fetch_optional(&state.db) 160 - .await; 161 - 162 - match record { 163 - Ok(Some(row)) => { 164 - let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr { 165 - applied: true, 166 - r#ref: Some(r.clone()), 167 - }); 168 - 169 - return ( 170 - StatusCode::OK, 171 - Json(SubjectStatus { 172 - subject: json!({ 173 - "$type": "com.atproto.repo.strongRef", 174 - "uri": uri, 175 - "cid": uri 176 - }), 177 - takedown, 178 - deactivated: None, 179 - }), 180 - ) 181 - .into_response(); 182 - } 183 - Ok(None) => { 184 - return ( 185 - StatusCode::NOT_FOUND, 186 - Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 187 - ) 188 - .into_response(); 189 - } 190 - Err(e) => { 191 - error!("DB error in get_subject_status: {:?}", e); 192 - return ( 193 - StatusCode::INTERNAL_SERVER_ERROR, 194 - Json(json!({"error": "InternalError"})), 195 - ) 196 - .into_response(); 197 - } 198 - } 199 - } 200 - 201 - if let Some(blob_cid) = &params.blob { 202 - let blob = sqlx::query!("SELECT cid, takedown_ref FROM blobs WHERE cid = $1", blob_cid) 203 - .fetch_optional(&state.db) 204 - .await; 205 - 206 - match blob { 207 - Ok(Some(row)) => { 208 - let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr { 209 - applied: true, 210 - r#ref: Some(r.clone()), 211 - }); 212 - 213 - return ( 214 - StatusCode::OK, 215 - Json(SubjectStatus { 216 - subject: json!({ 217 - "$type": "com.atproto.admin.defs#repoBlobRef", 218 - "did": "", 219 - "cid": row.cid 220 - }), 221 - takedown, 222 - deactivated: None, 223 - }), 224 - ) 225 - .into_response(); 226 - } 227 - Ok(None) => { 228 - return ( 229 - StatusCode::NOT_FOUND, 230 - Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 231 - ) 232 - .into_response(); 233 - } 234 - Err(e) => { 235 - error!("DB error in get_subject_status: {:?}", e); 236 - return ( 237 - StatusCode::INTERNAL_SERVER_ERROR, 238 - Json(json!({"error": "InternalError"})), 239 - ) 240 - .into_response(); 241 - } 242 - } 243 - } 244 - 245 - ( 246 - StatusCode::BAD_REQUEST, 247 - Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})), 248 - ) 249 - .into_response() 250 - } 251 - 252 - #[derive(Deserialize)] 253 - #[serde(rename_all = "camelCase")] 254 - pub struct UpdateSubjectStatusInput { 255 - pub subject: serde_json::Value, 256 - pub takedown: Option<StatusAttrInput>, 257 - pub deactivated: Option<StatusAttrInput>, 258 - } 259 - 260 - #[derive(Deserialize)] 261 - pub struct StatusAttrInput { 262 - pub apply: bool, 263 - pub r#ref: Option<String>, 264 - } 265 - 266 - pub async fn update_subject_status( 267 - State(state): State<AppState>, 268 - headers: axum::http::HeaderMap, 269 - Json(input): Json<UpdateSubjectStatusInput>, 270 - ) -> Response { 271 - let auth_header = headers.get("Authorization"); 272 - if auth_header.is_none() { 273 - return ( 274 - StatusCode::UNAUTHORIZED, 275 - Json(json!({"error": "AuthenticationRequired"})), 276 - ) 277 - .into_response(); 278 - } 279 - 280 - let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 281 - 282 - match subject_type { 283 - Some("com.atproto.admin.defs#repoRef") => { 284 - let did = input.subject.get("did").and_then(|d| d.as_str()); 285 - if let Some(did) = did { 286 - if let Some(takedown) = &input.takedown { 287 - let takedown_ref = if takedown.apply { 288 - takedown.r#ref.clone() 289 - } else { 290 - None 291 - }; 292 - let _ = sqlx::query!( 293 - "UPDATE users SET takedown_ref = $1 WHERE did = $2", 294 - takedown_ref, 295 - did 296 - ) 297 - .execute(&state.db) 298 - .await; 299 - } 300 - 301 - if let Some(deactivated) = &input.deactivated { 302 - if deactivated.apply { 303 - let _ = sqlx::query!( 304 - "UPDATE users SET deactivated_at = NOW() WHERE did = $1", 305 - did 306 - ) 307 - .execute(&state.db) 308 - .await; 309 - } else { 310 - let _ = sqlx::query!( 311 - "UPDATE users SET deactivated_at = NULL WHERE did = $1", 312 - did 313 - ) 314 - .execute(&state.db) 315 - .await; 316 - } 317 - } 318 - 319 - return ( 320 - StatusCode::OK, 321 - Json(json!({ 322 - "subject": input.subject, 323 - "takedown": input.takedown.as_ref().map(|t| json!({ 324 - "applied": t.apply, 325 - "ref": t.r#ref 326 - })), 327 - "deactivated": input.deactivated.as_ref().map(|d| json!({ 328 - "applied": d.apply 329 - })) 330 - })), 331 - ) 332 - .into_response(); 333 - } 334 - } 335 - Some("com.atproto.repo.strongRef") => { 336 - let uri = input.subject.get("uri").and_then(|u| u.as_str()); 337 - if let Some(uri) = uri { 338 - if let Some(takedown) = &input.takedown { 339 - let takedown_ref = if takedown.apply { 340 - takedown.r#ref.clone() 341 - } else { 342 - None 343 - }; 344 - let _ = sqlx::query!( 345 - "UPDATE records SET takedown_ref = $1 WHERE record_cid = $2", 346 - takedown_ref, 347 - uri 348 - ) 349 - .execute(&state.db) 350 - .await; 351 - } 352 - 353 - return ( 354 - StatusCode::OK, 355 - Json(json!({ 356 - "subject": input.subject, 357 - "takedown": input.takedown.as_ref().map(|t| json!({ 358 - "applied": t.apply, 359 - "ref": t.r#ref 360 - })) 361 - })), 362 - ) 363 - .into_response(); 364 - } 365 - } 366 - Some("com.atproto.admin.defs#repoBlobRef") => { 367 - let cid = input.subject.get("cid").and_then(|c| c.as_str()); 368 - if let Some(cid) = cid { 369 - if let Some(takedown) = &input.takedown { 370 - let takedown_ref = if takedown.apply { 371 - takedown.r#ref.clone() 372 - } else { 373 - None 374 - }; 375 - let _ = sqlx::query!( 376 - "UPDATE blobs SET takedown_ref = $1 WHERE cid = $2", 377 - takedown_ref, 378 - cid 379 - ) 380 - .execute(&state.db) 381 - .await; 382 - } 383 - 384 - return ( 385 - StatusCode::OK, 386 - Json(json!({ 387 - "subject": input.subject, 388 - "takedown": input.takedown.as_ref().map(|t| json!({ 389 - "applied": t.apply, 390 - "ref": t.r#ref 391 - })) 392 - })), 393 - ) 394 - .into_response(); 395 - } 396 - } 397 - _ => {} 398 - } 399 - 400 - ( 401 - StatusCode::BAD_REQUEST, 402 - Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})), 403 - ) 404 - .into_response() 405 - } 406 - 407 - #[derive(Deserialize)] 408 - pub struct GetInviteCodesParams { 409 - pub sort: Option<String>, 410 - pub limit: Option<i64>, 411 - pub cursor: Option<String>, 412 - } 413 - 414 - #[derive(Serialize)] 415 - #[serde(rename_all = "camelCase")] 416 - pub struct InviteCodeInfo { 417 - pub code: String, 418 - pub available: i32, 419 - pub disabled: bool, 420 - pub for_account: String, 421 - pub created_by: String, 422 - pub created_at: String, 423 - pub uses: Vec<InviteCodeUseInfo>, 424 - } 425 - 426 - #[derive(Serialize)] 427 - #[serde(rename_all = "camelCase")] 428 - pub struct InviteCodeUseInfo { 429 - pub used_by: String, 430 - pub used_at: String, 431 - } 432 - 433 - #[derive(Serialize)] 434 - pub struct GetInviteCodesOutput { 435 - pub cursor: Option<String>, 436 - pub codes: Vec<InviteCodeInfo>, 437 - } 438 - 439 - pub async fn get_invite_codes( 440 - State(state): State<AppState>, 441 - headers: axum::http::HeaderMap, 442 - Query(params): Query<GetInviteCodesParams>, 443 - ) -> Response { 444 - let auth_header = headers.get("Authorization"); 445 - if auth_header.is_none() { 446 - return ( 447 - StatusCode::UNAUTHORIZED, 448 - Json(json!({"error": "AuthenticationRequired"})), 449 - ) 450 - .into_response(); 451 - } 452 - 453 - let limit = params.limit.unwrap_or(100).min(500); 454 - let sort = params.sort.as_deref().unwrap_or("recent"); 455 - 456 - let order_clause = match sort { 457 - "usage" => "available_uses DESC", 458 - _ => "created_at DESC", 459 - }; 460 - 461 - let codes_result = if let Some(cursor) = &params.cursor { 462 - sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!( 463 - r#" 464 - SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 465 - FROM invite_codes ic 466 - WHERE ic.created_at < (SELECT created_at FROM invite_codes WHERE code = $1) 467 - ORDER BY {} 468 - LIMIT $2 469 - "#, 470 - order_clause 471 - )) 472 - .bind(cursor) 473 - .bind(limit) 474 - .fetch_all(&state.db) 475 - .await 476 - } else { 477 - sqlx::query_as::<_, (String, i32, Option<bool>, uuid::Uuid, chrono::DateTime<chrono::Utc>)>(&format!( 478 - r#" 479 - SELECT ic.code, ic.available_uses, ic.disabled, ic.created_by_user, ic.created_at 480 - FROM invite_codes ic 481 - ORDER BY {} 482 - LIMIT $1 483 - "#, 484 - order_clause 485 - )) 486 - .bind(limit) 487 - .fetch_all(&state.db) 488 - .await 489 - }; 490 - 491 - let codes_rows = match codes_result { 492 - Ok(rows) => rows, 493 - Err(e) => { 494 - error!("DB error fetching invite codes: {:?}", e); 495 - return ( 496 - StatusCode::INTERNAL_SERVER_ERROR, 497 - Json(json!({"error": "InternalError"})), 498 - ) 499 - .into_response(); 500 - } 501 - }; 502 - 503 - let mut codes = Vec::new(); 504 - for (code, available_uses, disabled, created_by_user, created_at) in &codes_rows { 505 - let creator_did = sqlx::query_scalar!("SELECT did FROM users WHERE id = $1", created_by_user) 506 - .fetch_optional(&state.db) 507 - .await 508 - .ok() 509 - .flatten() 510 - .unwrap_or_else(|| "unknown".to_string()); 511 - 512 - let uses_result = sqlx::query!( 513 - r#" 514 - SELECT u.did, icu.used_at 515 - FROM invite_code_uses icu 516 - JOIN users u ON icu.used_by_user = u.id 517 - WHERE icu.code = $1 518 - ORDER BY icu.used_at DESC 519 - "#, 520 - code 521 - ) 522 - .fetch_all(&state.db) 523 - .await; 524 - 525 - let uses = match uses_result { 526 - Ok(use_rows) => use_rows 527 - .iter() 528 - .map(|u| InviteCodeUseInfo { 529 - used_by: u.did.clone(), 530 - used_at: u.used_at.to_rfc3339(), 531 - }) 532 - .collect(), 533 - Err(_) => Vec::new(), 534 - }; 535 - 536 - codes.push(InviteCodeInfo { 537 - code: code.clone(), 538 - available: *available_uses, 539 - disabled: disabled.unwrap_or(false), 540 - for_account: creator_did.clone(), 541 - created_by: creator_did, 542 - created_at: created_at.to_rfc3339(), 543 - uses, 544 - }); 545 - } 546 - 547 - let next_cursor = if codes_rows.len() == limit as usize { 548 - codes_rows.last().map(|(code, _, _, _, _)| code.clone()) 549 - } else { 550 - None 551 - }; 552 - 553 - ( 554 - StatusCode::OK, 555 - Json(GetInviteCodesOutput { 556 - cursor: next_cursor, 557 - codes, 558 - }), 559 - ) 560 - .into_response() 561 - } 562 - 563 - #[derive(Deserialize)] 564 - pub struct DisableAccountInvitesInput { 565 - pub account: String, 566 - } 567 - 568 - pub async fn disable_account_invites( 569 - State(state): State<AppState>, 570 - headers: axum::http::HeaderMap, 571 - Json(input): Json<DisableAccountInvitesInput>, 572 - ) -> Response { 573 - let auth_header = headers.get("Authorization"); 574 - if auth_header.is_none() { 575 - return ( 576 - StatusCode::UNAUTHORIZED, 577 - Json(json!({"error": "AuthenticationRequired"})), 578 - ) 579 - .into_response(); 580 - } 581 - 582 - let account = input.account.trim(); 583 - if account.is_empty() { 584 - return ( 585 - StatusCode::BAD_REQUEST, 586 - Json(json!({"error": "InvalidRequest", "message": "account is required"})), 587 - ) 588 - .into_response(); 589 - } 590 - 591 - let result = sqlx::query!("UPDATE users SET invites_disabled = TRUE WHERE did = $1", account) 592 - .execute(&state.db) 593 - .await; 594 - 595 - match result { 596 - Ok(r) => { 597 - if r.rows_affected() == 0 { 598 - return ( 599 - StatusCode::NOT_FOUND, 600 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 601 - ) 602 - .into_response(); 603 - } 604 - (StatusCode::OK, Json(json!({}))).into_response() 605 - } 606 - Err(e) => { 607 - error!("DB error disabling account invites: {:?}", e); 608 - ( 609 - StatusCode::INTERNAL_SERVER_ERROR, 610 - Json(json!({"error": "InternalError"})), 611 - ) 612 - .into_response() 613 - } 614 - } 615 - } 616 - 617 - #[derive(Deserialize)] 618 - pub struct EnableAccountInvitesInput { 619 - pub account: String, 620 - } 621 - 622 - pub async fn enable_account_invites( 623 - State(state): State<AppState>, 624 - headers: axum::http::HeaderMap, 625 - Json(input): Json<EnableAccountInvitesInput>, 626 - ) -> Response { 627 - let auth_header = headers.get("Authorization"); 628 - if auth_header.is_none() { 629 - return ( 630 - StatusCode::UNAUTHORIZED, 631 - Json(json!({"error": "AuthenticationRequired"})), 632 - ) 633 - .into_response(); 634 - } 635 - 636 - let account = input.account.trim(); 637 - if account.is_empty() { 638 - return ( 639 - StatusCode::BAD_REQUEST, 640 - Json(json!({"error": "InvalidRequest", "message": "account is required"})), 641 - ) 642 - .into_response(); 643 - } 644 - 645 - let result = sqlx::query!("UPDATE users SET invites_disabled = FALSE WHERE did = $1", account) 646 - .execute(&state.db) 647 - .await; 648 - 649 - match result { 650 - Ok(r) => { 651 - if r.rows_affected() == 0 { 652 - return ( 653 - StatusCode::NOT_FOUND, 654 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 655 - ) 656 - .into_response(); 657 - } 658 - (StatusCode::OK, Json(json!({}))).into_response() 659 - } 660 - Err(e) => { 661 - error!("DB error enabling account invites: {:?}", e); 662 - ( 663 - StatusCode::INTERNAL_SERVER_ERROR, 664 - Json(json!({"error": "InternalError"})), 665 - ) 666 - .into_response() 667 - } 668 - } 669 - } 670 - 671 - #[derive(Deserialize)] 672 - pub struct GetAccountInfoParams { 673 - pub did: String, 674 - } 675 - 676 - #[derive(Serialize)] 677 - #[serde(rename_all = "camelCase")] 678 - pub struct AccountInfo { 679 - pub did: String, 680 - pub handle: String, 681 - pub email: Option<String>, 682 - pub indexed_at: String, 683 - pub invite_note: Option<String>, 684 - pub invites_disabled: bool, 685 - pub email_confirmed_at: Option<String>, 686 - pub deactivated_at: Option<String>, 687 - } 688 - 689 - #[derive(Serialize)] 690 - #[serde(rename_all = "camelCase")] 691 - pub struct GetAccountInfosOutput { 692 - pub infos: Vec<AccountInfo>, 693 - } 694 - 695 - pub async fn get_account_info( 696 - State(state): State<AppState>, 697 - headers: axum::http::HeaderMap, 698 - Query(params): Query<GetAccountInfoParams>, 699 - ) -> Response { 700 - let auth_header = headers.get("Authorization"); 701 - if auth_header.is_none() { 702 - return ( 703 - StatusCode::UNAUTHORIZED, 704 - Json(json!({"error": "AuthenticationRequired"})), 705 - ) 706 - .into_response(); 707 - } 708 - 709 - let did = params.did.trim(); 710 - if did.is_empty() { 711 - return ( 712 - StatusCode::BAD_REQUEST, 713 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 714 - ) 715 - .into_response(); 716 - } 717 - 718 - let result = sqlx::query!( 719 - r#" 720 - SELECT did, handle, email, created_at 721 - FROM users 722 - WHERE did = $1 723 - "#, 724 - did 725 - ) 726 - .fetch_optional(&state.db) 727 - .await; 728 - 729 - match result { 730 - Ok(Some(row)) => { 731 - ( 732 - StatusCode::OK, 733 - Json(AccountInfo { 734 - did: row.did, 735 - handle: row.handle, 736 - email: Some(row.email), 737 - indexed_at: row.created_at.to_rfc3339(), 738 - invite_note: None, 739 - invites_disabled: false, 740 - email_confirmed_at: None, 741 - deactivated_at: None, 742 - }), 743 - ) 744 - .into_response() 745 - } 746 - Ok(None) => ( 747 - StatusCode::NOT_FOUND, 748 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 749 - ) 750 - .into_response(), 751 - Err(e) => { 752 - error!("DB error in get_account_info: {:?}", e); 753 - ( 754 - StatusCode::INTERNAL_SERVER_ERROR, 755 - Json(json!({"error": "InternalError"})), 756 - ) 757 - .into_response() 758 - } 759 - } 760 - } 761 - 762 - #[derive(Deserialize)] 763 - pub struct GetAccountInfosParams { 764 - pub dids: String, 765 - } 766 - 767 - pub async fn get_account_infos( 768 - State(state): State<AppState>, 769 - headers: axum::http::HeaderMap, 770 - Query(params): Query<GetAccountInfosParams>, 771 - ) -> Response { 772 - let auth_header = headers.get("Authorization"); 773 - if auth_header.is_none() { 774 - return ( 775 - StatusCode::UNAUTHORIZED, 776 - Json(json!({"error": "AuthenticationRequired"})), 777 - ) 778 - .into_response(); 779 - } 780 - 781 - let dids: Vec<&str> = params.dids.split(',').map(|s| s.trim()).collect(); 782 - if dids.is_empty() { 783 - return ( 784 - StatusCode::BAD_REQUEST, 785 - Json(json!({"error": "InvalidRequest", "message": "dids is required"})), 786 - ) 787 - .into_response(); 788 - } 789 - 790 - let mut infos = Vec::new(); 791 - 792 - for did in dids { 793 - if did.is_empty() { 794 - continue; 795 - } 796 - 797 - let result = sqlx::query!( 798 - r#" 799 - SELECT did, handle, email, created_at 800 - FROM users 801 - WHERE did = $1 802 - "#, 803 - did 804 - ) 805 - .fetch_optional(&state.db) 806 - .await; 807 - 808 - if let Ok(Some(row)) = result { 809 - infos.push(AccountInfo { 810 - did: row.did, 811 - handle: row.handle, 812 - email: Some(row.email), 813 - indexed_at: row.created_at.to_rfc3339(), 814 - invite_note: None, 815 - invites_disabled: false, 816 - email_confirmed_at: None, 817 - deactivated_at: None, 818 - }); 819 - } 820 - } 821 - 822 - (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response() 823 - } 824 - 825 - #[derive(Deserialize)] 826 - pub struct DeleteAccountInput { 827 - pub did: String, 828 - } 829 - 830 - pub async fn delete_account( 831 - State(state): State<AppState>, 832 - headers: axum::http::HeaderMap, 833 - Json(input): Json<DeleteAccountInput>, 834 - ) -> Response { 835 - let auth_header = headers.get("Authorization"); 836 - if auth_header.is_none() { 837 - return ( 838 - StatusCode::UNAUTHORIZED, 839 - Json(json!({"error": "AuthenticationRequired"})), 840 - ) 841 - .into_response(); 842 - } 843 - 844 - let did = input.did.trim(); 845 - if did.is_empty() { 846 - return ( 847 - StatusCode::BAD_REQUEST, 848 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 849 - ) 850 - .into_response(); 851 - } 852 - 853 - let user = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 854 - .fetch_optional(&state.db) 855 - .await; 856 - 857 - let user_id = match user { 858 - Ok(Some(row)) => row.id, 859 - Ok(None) => { 860 - return ( 861 - StatusCode::NOT_FOUND, 862 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 863 - ) 864 - .into_response(); 865 - } 866 - Err(e) => { 867 - error!("DB error in delete_account: {:?}", e); 868 - return ( 869 - StatusCode::INTERNAL_SERVER_ERROR, 870 - Json(json!({"error": "InternalError"})), 871 - ) 872 - .into_response(); 873 - } 874 - }; 875 - 876 - let _ = sqlx::query!("DELETE FROM sessions WHERE did = $1", did) 877 - .execute(&state.db) 878 - .await; 879 - 880 - let _ = sqlx::query!("DELETE FROM records WHERE repo_id = $1", user_id) 881 - .execute(&state.db) 882 - .await; 883 - 884 - let _ = sqlx::query!("DELETE FROM repos WHERE user_id = $1", user_id) 885 - .execute(&state.db) 886 - .await; 887 - 888 - let _ = sqlx::query!("DELETE FROM blobs WHERE created_by_user = $1", user_id) 889 - .execute(&state.db) 890 - .await; 891 - 892 - let _ = sqlx::query!("DELETE FROM user_keys WHERE user_id = $1", user_id) 893 - .execute(&state.db) 894 - .await; 895 - 896 - let result = sqlx::query!("DELETE FROM users WHERE id = $1", user_id) 897 - .execute(&state.db) 898 - .await; 899 - 900 - match result { 901 - Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 902 - Err(e) => { 903 - error!("DB error deleting account: {:?}", e); 904 - ( 905 - StatusCode::INTERNAL_SERVER_ERROR, 906 - Json(json!({"error": "InternalError"})), 907 - ) 908 - .into_response() 909 - } 910 - } 911 - } 912 - 913 - #[derive(Deserialize)] 914 - pub struct UpdateAccountEmailInput { 915 - pub account: String, 916 - pub email: String, 917 - } 918 - 919 - pub async fn update_account_email( 920 - State(state): State<AppState>, 921 - headers: axum::http::HeaderMap, 922 - Json(input): Json<UpdateAccountEmailInput>, 923 - ) -> Response { 924 - let auth_header = headers.get("Authorization"); 925 - if auth_header.is_none() { 926 - return ( 927 - StatusCode::UNAUTHORIZED, 928 - Json(json!({"error": "AuthenticationRequired"})), 929 - ) 930 - .into_response(); 931 - } 932 - 933 - let account = input.account.trim(); 934 - let email = input.email.trim(); 935 - 936 - if account.is_empty() || email.is_empty() { 937 - return ( 938 - StatusCode::BAD_REQUEST, 939 - Json(json!({"error": "InvalidRequest", "message": "account and email are required"})), 940 - ) 941 - .into_response(); 942 - } 943 - 944 - let result = sqlx::query!("UPDATE users SET email = $1 WHERE did = $2", email, account) 945 - .execute(&state.db) 946 - .await; 947 - 948 - match result { 949 - Ok(r) => { 950 - if r.rows_affected() == 0 { 951 - return ( 952 - StatusCode::NOT_FOUND, 953 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 954 - ) 955 - .into_response(); 956 - } 957 - (StatusCode::OK, Json(json!({}))).into_response() 958 - } 959 - Err(e) => { 960 - error!("DB error updating email: {:?}", e); 961 - ( 962 - StatusCode::INTERNAL_SERVER_ERROR, 963 - Json(json!({"error": "InternalError"})), 964 - ) 965 - .into_response() 966 - } 967 - } 968 - } 969 - 970 - #[derive(Deserialize)] 971 - pub struct UpdateAccountHandleInput { 972 - pub did: String, 973 - pub handle: String, 974 - } 975 - 976 - pub async fn update_account_handle( 977 - State(state): State<AppState>, 978 - headers: axum::http::HeaderMap, 979 - Json(input): Json<UpdateAccountHandleInput>, 980 - ) -> Response { 981 - let auth_header = headers.get("Authorization"); 982 - if auth_header.is_none() { 983 - return ( 984 - StatusCode::UNAUTHORIZED, 985 - Json(json!({"error": "AuthenticationRequired"})), 986 - ) 987 - .into_response(); 988 - } 989 - 990 - let did = input.did.trim(); 991 - let handle = input.handle.trim(); 992 - 993 - if did.is_empty() || handle.is_empty() { 994 - return ( 995 - StatusCode::BAD_REQUEST, 996 - Json(json!({"error": "InvalidRequest", "message": "did and handle are required"})), 997 - ) 998 - .into_response(); 999 - } 1000 - 1001 - if !handle 1002 - .chars() 1003 - .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 1004 - { 1005 - return ( 1006 - StatusCode::BAD_REQUEST, 1007 - Json(json!({"error": "InvalidHandle", "message": "Handle contains invalid characters"})), 1008 - ) 1009 - .into_response(); 1010 - } 1011 - 1012 - let existing = sqlx::query!("SELECT id FROM users WHERE handle = $1 AND did != $2", handle, did) 1013 - .fetch_optional(&state.db) 1014 - .await; 1015 - 1016 - if let Ok(Some(_)) = existing { 1017 - return ( 1018 - StatusCode::BAD_REQUEST, 1019 - Json(json!({"error": "HandleTaken", "message": "Handle is already in use"})), 1020 - ) 1021 - .into_response(); 1022 - } 1023 - 1024 - let result = sqlx::query!("UPDATE users SET handle = $1 WHERE did = $2", handle, did) 1025 - .execute(&state.db) 1026 - .await; 1027 - 1028 - match result { 1029 - Ok(r) => { 1030 - if r.rows_affected() == 0 { 1031 - return ( 1032 - StatusCode::NOT_FOUND, 1033 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 1034 - ) 1035 - .into_response(); 1036 - } 1037 - (StatusCode::OK, Json(json!({}))).into_response() 1038 - } 1039 - Err(e) => { 1040 - error!("DB error updating handle: {:?}", e); 1041 - ( 1042 - StatusCode::INTERNAL_SERVER_ERROR, 1043 - Json(json!({"error": "InternalError"})), 1044 - ) 1045 - .into_response() 1046 - } 1047 - } 1048 - } 1049 - 1050 - #[derive(Deserialize)] 1051 - pub struct UpdateAccountPasswordInput { 1052 - pub did: String, 1053 - pub password: String, 1054 - } 1055 - 1056 - pub async fn update_account_password( 1057 - State(state): State<AppState>, 1058 - headers: axum::http::HeaderMap, 1059 - Json(input): Json<UpdateAccountPasswordInput>, 1060 - ) -> Response { 1061 - let auth_header = headers.get("Authorization"); 1062 - if auth_header.is_none() { 1063 - return ( 1064 - StatusCode::UNAUTHORIZED, 1065 - Json(json!({"error": "AuthenticationRequired"})), 1066 - ) 1067 - .into_response(); 1068 - } 1069 - 1070 - let did = input.did.trim(); 1071 - let password = input.password.trim(); 1072 - 1073 - if did.is_empty() || password.is_empty() { 1074 - return ( 1075 - StatusCode::BAD_REQUEST, 1076 - Json(json!({"error": "InvalidRequest", "message": "did and password are required"})), 1077 - ) 1078 - .into_response(); 1079 - } 1080 - 1081 - let password_hash = match bcrypt::hash(password, bcrypt::DEFAULT_COST) { 1082 - Ok(h) => h, 1083 - Err(e) => { 1084 - error!("Failed to hash password: {:?}", e); 1085 - return ( 1086 - StatusCode::INTERNAL_SERVER_ERROR, 1087 - Json(json!({"error": "InternalError"})), 1088 - ) 1089 - .into_response(); 1090 - } 1091 - }; 1092 - 1093 - let result = sqlx::query!("UPDATE users SET password_hash = $1 WHERE did = $2", password_hash, did) 1094 - .execute(&state.db) 1095 - .await; 1096 - 1097 - match result { 1098 - Ok(r) => { 1099 - if r.rows_affected() == 0 { 1100 - return ( 1101 - StatusCode::NOT_FOUND, 1102 - Json(json!({"error": "AccountNotFound", "message": "Account not found"})), 1103 - ) 1104 - .into_response(); 1105 - } 1106 - (StatusCode::OK, Json(json!({}))).into_response() 1107 - } 1108 - Err(e) => { 1109 - error!("DB error updating password: {:?}", e); 1110 - ( 1111 - StatusCode::INTERNAL_SERVER_ERROR, 1112 - Json(json!({"error": "InternalError"})), 1113 - ) 1114 - .into_response() 1115 - } 1116 - } 1117 - } 1118 - 1119 - #[derive(Deserialize)] 1120 - #[serde(rename_all = "camelCase")] 1121 - pub struct SendEmailInput { 1122 - pub recipient_did: String, 1123 - pub sender_did: String, 1124 - pub content: String, 1125 - pub subject: Option<String>, 1126 - pub comment: Option<String>, 1127 - } 1128 - 1129 - #[derive(Serialize)] 1130 - pub struct SendEmailOutput { 1131 - pub sent: bool, 1132 - } 1133 - 1134 - pub async fn send_email( 1135 - State(state): State<AppState>, 1136 - headers: axum::http::HeaderMap, 1137 - Json(input): Json<SendEmailInput>, 1138 - ) -> Response { 1139 - let auth_header = headers.get("Authorization"); 1140 - if auth_header.is_none() { 1141 - return ( 1142 - StatusCode::UNAUTHORIZED, 1143 - Json(json!({"error": "AuthenticationRequired"})), 1144 - ) 1145 - .into_response(); 1146 - } 1147 - 1148 - let recipient_did = input.recipient_did.trim(); 1149 - let content = input.content.trim(); 1150 - 1151 - if recipient_did.is_empty() { 1152 - return ( 1153 - StatusCode::BAD_REQUEST, 1154 - Json(json!({"error": "InvalidRequest", "message": "recipientDid is required"})), 1155 - ) 1156 - .into_response(); 1157 - } 1158 - 1159 - if content.is_empty() { 1160 - return ( 1161 - StatusCode::BAD_REQUEST, 1162 - Json(json!({"error": "InvalidRequest", "message": "content is required"})), 1163 - ) 1164 - .into_response(); 1165 - } 1166 - 1167 - let user = sqlx::query!( 1168 - "SELECT id, email, handle FROM users WHERE did = $1", 1169 - recipient_did 1170 - ) 1171 - .fetch_optional(&state.db) 1172 - .await; 1173 - 1174 - let (user_id, email, handle) = match user { 1175 - Ok(Some(row)) => (row.id, row.email, row.handle), 1176 - Ok(None) => { 1177 - return ( 1178 - StatusCode::NOT_FOUND, 1179 - Json(json!({"error": "AccountNotFound", "message": "Recipient account not found"})), 1180 - ) 1181 - .into_response(); 1182 - } 1183 - Err(e) => { 1184 - error!("DB error in send_email: {:?}", e); 1185 - return ( 1186 - StatusCode::INTERNAL_SERVER_ERROR, 1187 - Json(json!({"error": "InternalError"})), 1188 - ) 1189 - .into_response(); 1190 - } 1191 - }; 1192 - 1193 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1194 - let subject = input 1195 - .subject 1196 - .clone() 1197 - .unwrap_or_else(|| format!("Message from {}", hostname)); 1198 - 1199 - let notification = crate::notifications::NewNotification::email( 1200 - user_id, 1201 - crate::notifications::NotificationType::AdminEmail, 1202 - email, 1203 - subject, 1204 - content.to_string(), 1205 - ); 1206 - 1207 - let result = crate::notifications::enqueue_notification(&state.db, notification).await; 1208 - 1209 - match result { 1210 - Ok(_) => { 1211 - tracing::info!( 1212 - "Admin email queued for {} ({})", 1213 - handle, 1214 - recipient_did 1215 - ); 1216 - (StatusCode::OK, Json(SendEmailOutput { sent: true })).into_response() 1217 - } 1218 - Err(e) => { 1219 - warn!("Failed to enqueue admin email: {:?}", e); 1220 - (StatusCode::OK, Json(SendEmailOutput { sent: false })).into_response() 1221 - } 1222 - } 1223 - }
··· 1 + pub mod account; 2 + pub mod invite; 3 + pub mod status; 4 5 + pub use account::{ 6 + delete_account, get_account_info, get_account_infos, send_email, update_account_email, 7 + update_account_handle, update_account_password, 8 + }; 9 + pub use invite::{ 10 + disable_account_invites, disable_invite_codes, enable_account_invites, get_invite_codes, 11 + }; 12 + pub use status::{get_subject_status, update_subject_status};
+356
src/api/admin/status.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Query, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use serde::{Deserialize, Serialize}; 9 + use serde_json::json; 10 + use tracing::error; 11 + 12 + #[derive(Deserialize)] 13 + pub struct GetSubjectStatusParams { 14 + pub did: Option<String>, 15 + pub uri: Option<String>, 16 + pub blob: Option<String>, 17 + } 18 + 19 + #[derive(Serialize)] 20 + pub struct SubjectStatus { 21 + pub subject: serde_json::Value, 22 + pub takedown: Option<StatusAttr>, 23 + pub deactivated: Option<StatusAttr>, 24 + } 25 + 26 + #[derive(Serialize)] 27 + #[serde(rename_all = "camelCase")] 28 + pub struct StatusAttr { 29 + pub applied: bool, 30 + pub r#ref: Option<String>, 31 + } 32 + 33 + pub async fn get_subject_status( 34 + State(state): State<AppState>, 35 + headers: axum::http::HeaderMap, 36 + Query(params): Query<GetSubjectStatusParams>, 37 + ) -> Response { 38 + let auth_header = headers.get("Authorization"); 39 + if auth_header.is_none() { 40 + return ( 41 + StatusCode::UNAUTHORIZED, 42 + Json(json!({"error": "AuthenticationRequired"})), 43 + ) 44 + .into_response(); 45 + } 46 + 47 + if params.did.is_none() && params.uri.is_none() && params.blob.is_none() { 48 + return ( 49 + StatusCode::BAD_REQUEST, 50 + Json(json!({"error": "InvalidRequest", "message": "Must provide did, uri, or blob"})), 51 + ) 52 + .into_response(); 53 + } 54 + 55 + if let Some(did) = &params.did { 56 + let user = sqlx::query!( 57 + "SELECT did, deactivated_at, takedown_ref FROM users WHERE did = $1", 58 + did 59 + ) 60 + .fetch_optional(&state.db) 61 + .await; 62 + 63 + match user { 64 + Ok(Some(row)) => { 65 + let deactivated = row.deactivated_at.map(|_| StatusAttr { 66 + applied: true, 67 + r#ref: None, 68 + }); 69 + let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr { 70 + applied: true, 71 + r#ref: Some(r.clone()), 72 + }); 73 + 74 + return ( 75 + StatusCode::OK, 76 + Json(SubjectStatus { 77 + subject: json!({ 78 + "$type": "com.atproto.admin.defs#repoRef", 79 + "did": row.did 80 + }), 81 + takedown, 82 + deactivated, 83 + }), 84 + ) 85 + .into_response(); 86 + } 87 + Ok(None) => { 88 + return ( 89 + StatusCode::NOT_FOUND, 90 + Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 91 + ) 92 + .into_response(); 93 + } 94 + Err(e) => { 95 + error!("DB error in get_subject_status: {:?}", e); 96 + return ( 97 + StatusCode::INTERNAL_SERVER_ERROR, 98 + Json(json!({"error": "InternalError"})), 99 + ) 100 + .into_response(); 101 + } 102 + } 103 + } 104 + 105 + if let Some(uri) = &params.uri { 106 + let record = sqlx::query!( 107 + "SELECT r.id, r.takedown_ref FROM records r WHERE r.record_cid = $1", 108 + uri 109 + ) 110 + .fetch_optional(&state.db) 111 + .await; 112 + 113 + match record { 114 + Ok(Some(row)) => { 115 + let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr { 116 + applied: true, 117 + r#ref: Some(r.clone()), 118 + }); 119 + 120 + return ( 121 + StatusCode::OK, 122 + Json(SubjectStatus { 123 + subject: json!({ 124 + "$type": "com.atproto.repo.strongRef", 125 + "uri": uri, 126 + "cid": uri 127 + }), 128 + takedown, 129 + deactivated: None, 130 + }), 131 + ) 132 + .into_response(); 133 + } 134 + Ok(None) => { 135 + return ( 136 + StatusCode::NOT_FOUND, 137 + Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 138 + ) 139 + .into_response(); 140 + } 141 + Err(e) => { 142 + error!("DB error in get_subject_status: {:?}", e); 143 + return ( 144 + StatusCode::INTERNAL_SERVER_ERROR, 145 + Json(json!({"error": "InternalError"})), 146 + ) 147 + .into_response(); 148 + } 149 + } 150 + } 151 + 152 + if let Some(blob_cid) = &params.blob { 153 + let blob = sqlx::query!("SELECT cid, takedown_ref FROM blobs WHERE cid = $1", blob_cid) 154 + .fetch_optional(&state.db) 155 + .await; 156 + 157 + match blob { 158 + Ok(Some(row)) => { 159 + let takedown = row.takedown_ref.as_ref().map(|r| StatusAttr { 160 + applied: true, 161 + r#ref: Some(r.clone()), 162 + }); 163 + 164 + return ( 165 + StatusCode::OK, 166 + Json(SubjectStatus { 167 + subject: json!({ 168 + "$type": "com.atproto.admin.defs#repoBlobRef", 169 + "did": "", 170 + "cid": row.cid 171 + }), 172 + takedown, 173 + deactivated: None, 174 + }), 175 + ) 176 + .into_response(); 177 + } 178 + Ok(None) => { 179 + return ( 180 + StatusCode::NOT_FOUND, 181 + Json(json!({"error": "SubjectNotFound", "message": "Subject not found"})), 182 + ) 183 + .into_response(); 184 + } 185 + Err(e) => { 186 + error!("DB error in get_subject_status: {:?}", e); 187 + return ( 188 + StatusCode::INTERNAL_SERVER_ERROR, 189 + Json(json!({"error": "InternalError"})), 190 + ) 191 + .into_response(); 192 + } 193 + } 194 + } 195 + 196 + ( 197 + StatusCode::BAD_REQUEST, 198 + Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})), 199 + ) 200 + .into_response() 201 + } 202 + 203 + #[derive(Deserialize)] 204 + #[serde(rename_all = "camelCase")] 205 + pub struct UpdateSubjectStatusInput { 206 + pub subject: serde_json::Value, 207 + pub takedown: Option<StatusAttrInput>, 208 + pub deactivated: Option<StatusAttrInput>, 209 + } 210 + 211 + #[derive(Deserialize)] 212 + pub struct StatusAttrInput { 213 + pub apply: bool, 214 + pub r#ref: Option<String>, 215 + } 216 + 217 + pub async fn update_subject_status( 218 + State(state): State<AppState>, 219 + headers: axum::http::HeaderMap, 220 + Json(input): Json<UpdateSubjectStatusInput>, 221 + ) -> Response { 222 + let auth_header = headers.get("Authorization"); 223 + if auth_header.is_none() { 224 + return ( 225 + StatusCode::UNAUTHORIZED, 226 + Json(json!({"error": "AuthenticationRequired"})), 227 + ) 228 + .into_response(); 229 + } 230 + 231 + let subject_type = input.subject.get("$type").and_then(|t| t.as_str()); 232 + 233 + match subject_type { 234 + Some("com.atproto.admin.defs#repoRef") => { 235 + let did = input.subject.get("did").and_then(|d| d.as_str()); 236 + if let Some(did) = did { 237 + if let Some(takedown) = &input.takedown { 238 + let takedown_ref = if takedown.apply { 239 + takedown.r#ref.clone() 240 + } else { 241 + None 242 + }; 243 + let _ = sqlx::query!( 244 + "UPDATE users SET takedown_ref = $1 WHERE did = $2", 245 + takedown_ref, 246 + did 247 + ) 248 + .execute(&state.db) 249 + .await; 250 + } 251 + 252 + if let Some(deactivated) = &input.deactivated { 253 + if deactivated.apply { 254 + let _ = sqlx::query!( 255 + "UPDATE users SET deactivated_at = NOW() WHERE did = $1", 256 + did 257 + ) 258 + .execute(&state.db) 259 + .await; 260 + } else { 261 + let _ = sqlx::query!( 262 + "UPDATE users SET deactivated_at = NULL WHERE did = $1", 263 + did 264 + ) 265 + .execute(&state.db) 266 + .await; 267 + } 268 + } 269 + 270 + return ( 271 + StatusCode::OK, 272 + Json(json!({ 273 + "subject": input.subject, 274 + "takedown": input.takedown.as_ref().map(|t| json!({ 275 + "applied": t.apply, 276 + "ref": t.r#ref 277 + })), 278 + "deactivated": input.deactivated.as_ref().map(|d| json!({ 279 + "applied": d.apply 280 + })) 281 + })), 282 + ) 283 + .into_response(); 284 + } 285 + } 286 + Some("com.atproto.repo.strongRef") => { 287 + let uri = input.subject.get("uri").and_then(|u| u.as_str()); 288 + if let Some(uri) = uri { 289 + if let Some(takedown) = &input.takedown { 290 + let takedown_ref = if takedown.apply { 291 + takedown.r#ref.clone() 292 + } else { 293 + None 294 + }; 295 + let _ = sqlx::query!( 296 + "UPDATE records SET takedown_ref = $1 WHERE record_cid = $2", 297 + takedown_ref, 298 + uri 299 + ) 300 + .execute(&state.db) 301 + .await; 302 + } 303 + 304 + return ( 305 + StatusCode::OK, 306 + Json(json!({ 307 + "subject": input.subject, 308 + "takedown": input.takedown.as_ref().map(|t| json!({ 309 + "applied": t.apply, 310 + "ref": t.r#ref 311 + })) 312 + })), 313 + ) 314 + .into_response(); 315 + } 316 + } 317 + Some("com.atproto.admin.defs#repoBlobRef") => { 318 + let cid = input.subject.get("cid").and_then(|c| c.as_str()); 319 + if let Some(cid) = cid { 320 + if let Some(takedown) = &input.takedown { 321 + let takedown_ref = if takedown.apply { 322 + takedown.r#ref.clone() 323 + } else { 324 + None 325 + }; 326 + let _ = sqlx::query!( 327 + "UPDATE blobs SET takedown_ref = $1 WHERE cid = $2", 328 + takedown_ref, 329 + cid 330 + ) 331 + .execute(&state.db) 332 + .await; 333 + } 334 + 335 + return ( 336 + StatusCode::OK, 337 + Json(json!({ 338 + "subject": input.subject, 339 + "takedown": input.takedown.as_ref().map(|t| json!({ 340 + "applied": t.apply, 341 + "ref": t.r#ref 342 + })) 343 + })), 344 + ) 345 + .into_response(); 346 + } 347 + } 348 + _ => {} 349 + } 350 + 351 + ( 352 + StatusCode::BAD_REQUEST, 353 + Json(json!({"error": "InvalidRequest", "message": "Invalid subject type"})), 354 + ) 355 + .into_response() 356 + }
+393
src/api/server/account_status.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use chrono::{Duration, Utc}; 9 + use serde::{Deserialize, Serialize}; 10 + use serde_json::json; 11 + use tracing::{error, info, warn}; 12 + use uuid::Uuid; 13 + 14 + #[derive(Serialize)] 15 + #[serde(rename_all = "camelCase")] 16 + pub 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 + 28 + pub 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 + 137 + pub 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")] 215 + pub struct DeactivateAccountInput { 216 + pub delete_after: Option<String>, 217 + } 218 + 219 + pub 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 + 296 + pub 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 + }
+366
src/api/server/app_password.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use serde::{Deserialize, Serialize}; 9 + use serde_json::json; 10 + use tracing::error; 11 + 12 + #[derive(Serialize)] 13 + #[serde(rename_all = "camelCase")] 14 + pub struct AppPassword { 15 + pub name: String, 16 + pub created_at: String, 17 + pub privileged: bool, 18 + } 19 + 20 + #[derive(Serialize)] 21 + pub struct ListAppPasswordsOutput { 22 + pub passwords: Vec<AppPassword>, 23 + } 24 + 25 + pub async fn list_app_passwords( 26 + State(state): State<AppState>, 27 + headers: axum::http::HeaderMap, 28 + ) -> Response { 29 + let auth_header = headers.get("Authorization"); 30 + if auth_header.is_none() { 31 + return ( 32 + StatusCode::UNAUTHORIZED, 33 + Json(json!({"error": "AuthenticationRequired"})), 34 + ) 35 + .into_response(); 36 + } 37 + 38 + let token = auth_header 39 + .unwrap() 40 + .to_str() 41 + .unwrap_or("") 42 + .replace("Bearer ", ""); 43 + 44 + let session = sqlx::query!( 45 + r#" 46 + SELECT s.did, k.key_bytes, u.id as user_id 47 + FROM sessions s 48 + JOIN users u ON s.did = u.did 49 + JOIN user_keys k ON u.id = k.user_id 50 + WHERE s.access_jwt = $1 51 + "#, 52 + token 53 + ) 54 + .fetch_optional(&state.db) 55 + .await; 56 + 57 + let (_did, key_bytes, user_id) = match session { 58 + Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 59 + Ok(None) => { 60 + return ( 61 + StatusCode::UNAUTHORIZED, 62 + Json(json!({"error": "AuthenticationFailed"})), 63 + ) 64 + .into_response(); 65 + } 66 + Err(e) => { 67 + error!("DB error in list_app_passwords: {:?}", e); 68 + return ( 69 + StatusCode::INTERNAL_SERVER_ERROR, 70 + Json(json!({"error": "InternalError"})), 71 + ) 72 + .into_response(); 73 + } 74 + }; 75 + 76 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 77 + return ( 78 + StatusCode::UNAUTHORIZED, 79 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 80 + ) 81 + .into_response(); 82 + } 83 + 84 + let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id) 85 + .fetch_all(&state.db) 86 + .await; 87 + 88 + match result { 89 + Ok(rows) => { 90 + let passwords: Vec<AppPassword> = rows 91 + .iter() 92 + .map(|row| { 93 + AppPassword { 94 + name: row.name.clone(), 95 + created_at: row.created_at.to_rfc3339(), 96 + privileged: row.privileged, 97 + } 98 + }) 99 + .collect(); 100 + 101 + (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response() 102 + } 103 + Err(e) => { 104 + error!("DB error listing app passwords: {:?}", e); 105 + ( 106 + StatusCode::INTERNAL_SERVER_ERROR, 107 + Json(json!({"error": "InternalError"})), 108 + ) 109 + .into_response() 110 + } 111 + } 112 + } 113 + 114 + #[derive(Deserialize)] 115 + pub struct CreateAppPasswordInput { 116 + pub name: String, 117 + pub privileged: Option<bool>, 118 + } 119 + 120 + #[derive(Serialize)] 121 + #[serde(rename_all = "camelCase")] 122 + pub struct CreateAppPasswordOutput { 123 + pub name: String, 124 + pub password: String, 125 + pub created_at: String, 126 + pub privileged: bool, 127 + } 128 + 129 + pub async fn create_app_password( 130 + State(state): State<AppState>, 131 + headers: axum::http::HeaderMap, 132 + Json(input): Json<CreateAppPasswordInput>, 133 + ) -> Response { 134 + let auth_header = headers.get("Authorization"); 135 + if auth_header.is_none() { 136 + return ( 137 + StatusCode::UNAUTHORIZED, 138 + Json(json!({"error": "AuthenticationRequired"})), 139 + ) 140 + .into_response(); 141 + } 142 + 143 + let token = auth_header 144 + .unwrap() 145 + .to_str() 146 + .unwrap_or("") 147 + .replace("Bearer ", ""); 148 + 149 + let session = sqlx::query!( 150 + r#" 151 + SELECT s.did, k.key_bytes, u.id as user_id 152 + FROM sessions s 153 + JOIN users u ON s.did = u.did 154 + JOIN user_keys k ON u.id = k.user_id 155 + WHERE s.access_jwt = $1 156 + "#, 157 + token 158 + ) 159 + .fetch_optional(&state.db) 160 + .await; 161 + 162 + let (_did, key_bytes, user_id) = match session { 163 + Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 164 + Ok(None) => { 165 + return ( 166 + StatusCode::UNAUTHORIZED, 167 + Json(json!({"error": "AuthenticationFailed"})), 168 + ) 169 + .into_response(); 170 + } 171 + Err(e) => { 172 + error!("DB error in create_app_password: {:?}", e); 173 + return ( 174 + StatusCode::INTERNAL_SERVER_ERROR, 175 + Json(json!({"error": "InternalError"})), 176 + ) 177 + .into_response(); 178 + } 179 + }; 180 + 181 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 182 + return ( 183 + StatusCode::UNAUTHORIZED, 184 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 185 + ) 186 + .into_response(); 187 + } 188 + 189 + let name = input.name.trim(); 190 + if name.is_empty() { 191 + return ( 192 + StatusCode::BAD_REQUEST, 193 + Json(json!({"error": "InvalidRequest", "message": "name is required"})), 194 + ) 195 + .into_response(); 196 + } 197 + 198 + let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 199 + .fetch_optional(&state.db) 200 + .await; 201 + 202 + if let Ok(Some(_)) = existing { 203 + return ( 204 + StatusCode::BAD_REQUEST, 205 + Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})), 206 + ) 207 + .into_response(); 208 + } 209 + 210 + let password: String = (0..4) 211 + .map(|_| { 212 + use rand::Rng; 213 + let mut rng = rand::thread_rng(); 214 + let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 215 + (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>() 216 + }) 217 + .collect::<Vec<String>>() 218 + .join("-"); 219 + 220 + let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) { 221 + Ok(h) => h, 222 + Err(e) => { 223 + error!("Failed to hash password: {:?}", e); 224 + return ( 225 + StatusCode::INTERNAL_SERVER_ERROR, 226 + Json(json!({"error": "InternalError"})), 227 + ) 228 + .into_response(); 229 + } 230 + }; 231 + 232 + let privileged = input.privileged.unwrap_or(false); 233 + let created_at = chrono::Utc::now(); 234 + 235 + let result = sqlx::query!( 236 + "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)", 237 + user_id, 238 + name, 239 + password_hash, 240 + created_at, 241 + privileged 242 + ) 243 + .execute(&state.db) 244 + .await; 245 + 246 + match result { 247 + Ok(_) => ( 248 + StatusCode::OK, 249 + Json(CreateAppPasswordOutput { 250 + name: name.to_string(), 251 + password, 252 + created_at: created_at.to_rfc3339(), 253 + privileged, 254 + }), 255 + ) 256 + .into_response(), 257 + Err(e) => { 258 + error!("DB error creating app password: {:?}", e); 259 + ( 260 + StatusCode::INTERNAL_SERVER_ERROR, 261 + Json(json!({"error": "InternalError"})), 262 + ) 263 + .into_response() 264 + } 265 + } 266 + } 267 + 268 + #[derive(Deserialize)] 269 + pub struct RevokeAppPasswordInput { 270 + pub name: String, 271 + } 272 + 273 + pub async fn revoke_app_password( 274 + State(state): State<AppState>, 275 + headers: axum::http::HeaderMap, 276 + Json(input): Json<RevokeAppPasswordInput>, 277 + ) -> Response { 278 + let auth_header = headers.get("Authorization"); 279 + if auth_header.is_none() { 280 + return ( 281 + StatusCode::UNAUTHORIZED, 282 + Json(json!({"error": "AuthenticationRequired"})), 283 + ) 284 + .into_response(); 285 + } 286 + 287 + let token = auth_header 288 + .unwrap() 289 + .to_str() 290 + .unwrap_or("") 291 + .replace("Bearer ", ""); 292 + 293 + let session = sqlx::query!( 294 + r#" 295 + SELECT s.did, k.key_bytes, u.id as user_id 296 + FROM sessions s 297 + JOIN users u ON s.did = u.did 298 + JOIN user_keys k ON u.id = k.user_id 299 + WHERE s.access_jwt = $1 300 + "#, 301 + token 302 + ) 303 + .fetch_optional(&state.db) 304 + .await; 305 + 306 + let (_did, key_bytes, user_id) = match session { 307 + Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 308 + Ok(None) => { 309 + return ( 310 + StatusCode::UNAUTHORIZED, 311 + Json(json!({"error": "AuthenticationFailed"})), 312 + ) 313 + .into_response(); 314 + } 315 + Err(e) => { 316 + error!("DB error in revoke_app_password: {:?}", e); 317 + return ( 318 + StatusCode::INTERNAL_SERVER_ERROR, 319 + Json(json!({"error": "InternalError"})), 320 + ) 321 + .into_response(); 322 + } 323 + }; 324 + 325 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 326 + return ( 327 + StatusCode::UNAUTHORIZED, 328 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 329 + ) 330 + .into_response(); 331 + } 332 + 333 + let name = input.name.trim(); 334 + if name.is_empty() { 335 + return ( 336 + StatusCode::BAD_REQUEST, 337 + Json(json!({"error": "InvalidRequest", "message": "name is required"})), 338 + ) 339 + .into_response(); 340 + } 341 + 342 + let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 343 + .execute(&state.db) 344 + .await; 345 + 346 + match result { 347 + Ok(r) => { 348 + if r.rows_affected() == 0 { 349 + return ( 350 + StatusCode::NOT_FOUND, 351 + Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})), 352 + ) 353 + .into_response(); 354 + } 355 + (StatusCode::OK, Json(json!({}))).into_response() 356 + } 357 + Err(e) => { 358 + error!("DB error revoking app password: {:?}", e); 359 + ( 360 + StatusCode::INTERNAL_SERVER_ERROR, 361 + Json(json!({"error": "InternalError"})), 362 + ) 363 + .into_response() 364 + } 365 + } 366 + }
+288
src/api/server/email.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use chrono::{Duration, Utc}; 9 + use rand::Rng; 10 + use serde::Deserialize; 11 + use serde_json::json; 12 + use tracing::{error, info, warn}; 13 + 14 + fn generate_confirmation_code() -> String { 15 + let mut rng = rand::thread_rng(); 16 + let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 17 + let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); 18 + let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); 19 + format!("{}-{}", part1, part2) 20 + } 21 + 22 + #[derive(Deserialize)] 23 + #[serde(rename_all = "camelCase")] 24 + pub struct RequestEmailUpdateInput { 25 + pub email: String, 26 + } 27 + 28 + pub async fn request_email_update( 29 + State(state): State<AppState>, 30 + headers: axum::http::HeaderMap, 31 + Json(input): Json<RequestEmailUpdateInput>, 32 + ) -> Response { 33 + let auth_header = headers.get("Authorization"); 34 + if auth_header.is_none() { 35 + return ( 36 + StatusCode::UNAUTHORIZED, 37 + Json(json!({"error": "AuthenticationRequired"})), 38 + ) 39 + .into_response(); 40 + } 41 + 42 + let token = auth_header 43 + .unwrap() 44 + .to_str() 45 + .unwrap_or("") 46 + .replace("Bearer ", ""); 47 + 48 + let session = sqlx::query!( 49 + r#" 50 + SELECT s.did, k.key_bytes, u.id as user_id, u.handle 51 + FROM sessions s 52 + JOIN users u ON s.did = u.did 53 + JOIN user_keys k ON u.id = k.user_id 54 + WHERE s.access_jwt = $1 55 + "#, 56 + token 57 + ) 58 + .fetch_optional(&state.db) 59 + .await; 60 + 61 + let (_did, key_bytes, user_id, handle) = match session { 62 + Ok(Some(row)) => (row.did, row.key_bytes, row.user_id, row.handle), 63 + Ok(None) => { 64 + return ( 65 + StatusCode::UNAUTHORIZED, 66 + Json(json!({"error": "AuthenticationFailed"})), 67 + ) 68 + .into_response(); 69 + } 70 + Err(e) => { 71 + error!("DB error in request_email_update: {:?}", e); 72 + return ( 73 + StatusCode::INTERNAL_SERVER_ERROR, 74 + Json(json!({"error": "InternalError"})), 75 + ) 76 + .into_response(); 77 + } 78 + }; 79 + 80 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 81 + return ( 82 + StatusCode::UNAUTHORIZED, 83 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 84 + ) 85 + .into_response(); 86 + } 87 + 88 + let email = input.email.trim().to_lowercase(); 89 + if email.is_empty() { 90 + return ( 91 + StatusCode::BAD_REQUEST, 92 + Json(json!({"error": "InvalidRequest", "message": "email is required"})), 93 + ) 94 + .into_response(); 95 + } 96 + 97 + let exists = sqlx::query!("SELECT 1 as one FROM users WHERE LOWER(email) = $1", email) 98 + .fetch_optional(&state.db) 99 + .await; 100 + 101 + if let Ok(Some(_)) = exists { 102 + return ( 103 + StatusCode::BAD_REQUEST, 104 + Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 105 + ) 106 + .into_response(); 107 + } 108 + 109 + let code = generate_confirmation_code(); 110 + let expires_at = Utc::now() + Duration::minutes(10); 111 + 112 + let update = sqlx::query!( 113 + "UPDATE users SET email_pending_verification = $1, email_confirmation_code = $2, email_confirmation_code_expires_at = $3 WHERE id = $4", 114 + email, 115 + code, 116 + expires_at, 117 + user_id 118 + ) 119 + .execute(&state.db) 120 + .await; 121 + 122 + if let Err(e) = update { 123 + error!("DB error setting email update code: {:?}", e); 124 + return ( 125 + StatusCode::INTERNAL_SERVER_ERROR, 126 + Json(json!({"error": "InternalError"})), 127 + ) 128 + .into_response(); 129 + } 130 + 131 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 132 + if let Err(e) = crate::notifications::enqueue_email_update( 133 + &state.db, 134 + user_id, 135 + &email, 136 + &handle, 137 + &code, 138 + &hostname, 139 + ) 140 + .await 141 + { 142 + warn!("Failed to enqueue email update notification: {:?}", e); 143 + } 144 + 145 + info!("Email update requested for user {}", user_id); 146 + 147 + (StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response() 148 + } 149 + 150 + #[derive(Deserialize)] 151 + #[serde(rename_all = "camelCase")] 152 + pub struct ConfirmEmailInput { 153 + pub email: String, 154 + pub token: String, 155 + } 156 + 157 + pub async fn confirm_email( 158 + State(state): State<AppState>, 159 + headers: axum::http::HeaderMap, 160 + Json(input): Json<ConfirmEmailInput>, 161 + ) -> Response { 162 + let auth_header = headers.get("Authorization"); 163 + if auth_header.is_none() { 164 + return ( 165 + StatusCode::UNAUTHORIZED, 166 + Json(json!({"error": "AuthenticationRequired"})), 167 + ) 168 + .into_response(); 169 + } 170 + 171 + let token = auth_header 172 + .unwrap() 173 + .to_str() 174 + .unwrap_or("") 175 + .replace("Bearer ", ""); 176 + 177 + let session = sqlx::query!( 178 + r#" 179 + SELECT s.did, k.key_bytes, u.id as user_id, u.email_confirmation_code, u.email_confirmation_code_expires_at, u.email_pending_verification 180 + FROM sessions s 181 + JOIN users u ON s.did = u.did 182 + JOIN user_keys k ON u.id = k.user_id 183 + WHERE s.access_jwt = $1 184 + "#, 185 + token 186 + ) 187 + .fetch_optional(&state.db) 188 + .await; 189 + 190 + let (_did, key_bytes, user_id, stored_code, expires_at, email_pending_verification) = match session { 191 + Ok(Some(row)) => ( 192 + row.did, 193 + row.key_bytes, 194 + row.user_id, 195 + row.email_confirmation_code, 196 + row.email_confirmation_code_expires_at, 197 + row.email_pending_verification, 198 + ), 199 + Ok(None) => { 200 + return ( 201 + StatusCode::UNAUTHORIZED, 202 + Json(json!({"error": "AuthenticationFailed"})), 203 + ) 204 + .into_response(); 205 + } 206 + Err(e) => { 207 + error!("DB error in confirm_email: {:?}", e); 208 + return ( 209 + StatusCode::INTERNAL_SERVER_ERROR, 210 + Json(json!({"error": "InternalError"})), 211 + ) 212 + .into_response(); 213 + } 214 + }; 215 + 216 + if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 217 + return ( 218 + StatusCode::UNAUTHORIZED, 219 + Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 220 + ) 221 + .into_response(); 222 + } 223 + 224 + let email = input.email.trim().to_lowercase(); 225 + let confirmation_code = input.token.trim(); 226 + 227 + if email_pending_verification.is_none() || stored_code.is_none() || expires_at.is_none() { 228 + return ( 229 + StatusCode::BAD_REQUEST, 230 + Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 231 + ) 232 + .into_response(); 233 + } 234 + 235 + let email_pending_verification = email_pending_verification.unwrap(); 236 + if email_pending_verification != email { 237 + return ( 238 + StatusCode::BAD_REQUEST, 239 + Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 240 + ) 241 + .into_response(); 242 + } 243 + 244 + if stored_code.unwrap() != confirmation_code { 245 + return ( 246 + StatusCode::BAD_REQUEST, 247 + Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 248 + ) 249 + .into_response(); 250 + } 251 + 252 + if Utc::now() > expires_at.unwrap() { 253 + return ( 254 + StatusCode::BAD_REQUEST, 255 + Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 256 + ) 257 + .into_response(); 258 + } 259 + 260 + let update = sqlx::query!( 261 + "UPDATE users SET email = $1, email_pending_verification = NULL, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE id = $2", 262 + email_pending_verification, 263 + user_id 264 + ) 265 + .execute(&state.db) 266 + .await; 267 + 268 + if let Err(e) = update { 269 + error!("DB error finalizing email update: {:?}", e); 270 + if e.as_database_error().map(|db_err| db_err.is_unique_violation()).unwrap_or(false) { 271 + return ( 272 + StatusCode::BAD_REQUEST, 273 + Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 274 + ) 275 + .into_response(); 276 + } 277 + 278 + return ( 279 + StatusCode::INTERNAL_SERVER_ERROR, 280 + Json(json!({"error": "InternalError"})), 281 + ) 282 + .into_response(); 283 + } 284 + 285 + info!("Email updated for user {}", user_id); 286 + 287 + (StatusCode::OK, Json(json!({}))).into_response() 288 + }
+11 -4
src/api/server/mod.rs
··· 1 pub mod invite; 2 pub mod meta; 3 pub mod session; 4 5 pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes}; 6 pub use meta::{describe_server, health}; 7 pub use session::{ 8 - activate_account, check_account_status, confirm_email, create_app_password, create_session, 9 - deactivate_account, delete_session, get_service_auth, get_session, list_app_passwords, 10 - refresh_session, request_account_delete, request_email_update, request_password_reset, 11 - reset_password, revoke_app_password, 12 };
··· 1 + pub mod account_status; 2 + pub mod app_password; 3 + pub mod email; 4 pub mod invite; 5 pub mod meta; 6 + pub mod password; 7 pub mod session; 8 9 + pub use account_status::{ 10 + activate_account, check_account_status, deactivate_account, request_account_delete, 11 + }; 12 + pub use app_password::{create_app_password, list_app_passwords, revoke_app_password}; 13 + pub use email::{confirm_email, request_email_update}; 14 pub use invite::{create_invite_code, create_invite_codes, get_account_invite_codes}; 15 pub use meta::{describe_server, health}; 16 + pub use password::{request_password_reset, reset_password}; 17 pub use session::{ 18 + create_session, delete_session, get_service_auth, get_session, refresh_session, 19 };
+221
src/api/server/password.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::State, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use bcrypt::{hash, DEFAULT_COST}; 9 + use chrono::{Duration, Utc}; 10 + use rand::Rng; 11 + use serde::Deserialize; 12 + use serde_json::json; 13 + use tracing::{error, info, warn}; 14 + 15 + fn generate_reset_code() -> String { 16 + let mut rng = rand::thread_rng(); 17 + let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 18 + let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); 19 + let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); 20 + format!("{}-{}", part1, part2) 21 + } 22 + 23 + #[derive(Deserialize)] 24 + pub struct RequestPasswordResetInput { 25 + pub email: String, 26 + } 27 + 28 + pub async fn request_password_reset( 29 + State(state): State<AppState>, 30 + Json(input): Json<RequestPasswordResetInput>, 31 + ) -> Response { 32 + let email = input.email.trim().to_lowercase(); 33 + if email.is_empty() { 34 + return ( 35 + StatusCode::BAD_REQUEST, 36 + Json(json!({"error": "InvalidRequest", "message": "email is required"})), 37 + ) 38 + .into_response(); 39 + } 40 + 41 + let user = sqlx::query!( 42 + "SELECT id, handle FROM users WHERE LOWER(email) = $1", 43 + email 44 + ) 45 + .fetch_optional(&state.db) 46 + .await; 47 + 48 + let (user_id, handle) = match user { 49 + Ok(Some(row)) => (row.id, row.handle), 50 + Ok(None) => { 51 + info!("Password reset requested for unknown email: {}", email); 52 + return (StatusCode::OK, Json(json!({}))).into_response(); 53 + } 54 + Err(e) => { 55 + error!("DB error in request_password_reset: {:?}", e); 56 + return ( 57 + StatusCode::INTERNAL_SERVER_ERROR, 58 + Json(json!({"error": "InternalError"})), 59 + ) 60 + .into_response(); 61 + } 62 + }; 63 + 64 + let code = generate_reset_code(); 65 + let expires_at = Utc::now() + Duration::minutes(10); 66 + 67 + let update = sqlx::query!( 68 + "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", 69 + code, 70 + expires_at, 71 + user_id 72 + ) 73 + .execute(&state.db) 74 + .await; 75 + 76 + if let Err(e) = update { 77 + error!("DB error setting reset code: {:?}", e); 78 + return ( 79 + StatusCode::INTERNAL_SERVER_ERROR, 80 + Json(json!({"error": "InternalError"})), 81 + ) 82 + .into_response(); 83 + } 84 + 85 + let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 86 + if let Err(e) = crate::notifications::enqueue_password_reset( 87 + &state.db, 88 + user_id, 89 + &email, 90 + &handle, 91 + &code, 92 + &hostname, 93 + ) 94 + .await 95 + { 96 + warn!("Failed to enqueue password reset notification: {:?}", e); 97 + } 98 + 99 + info!("Password reset requested for user {}", user_id); 100 + 101 + (StatusCode::OK, Json(json!({}))).into_response() 102 + } 103 + 104 + #[derive(Deserialize)] 105 + pub struct ResetPasswordInput { 106 + pub token: String, 107 + pub password: String, 108 + } 109 + 110 + pub async fn reset_password( 111 + State(state): State<AppState>, 112 + Json(input): Json<ResetPasswordInput>, 113 + ) -> Response { 114 + let token = input.token.trim(); 115 + let password = &input.password; 116 + 117 + if token.is_empty() { 118 + return ( 119 + StatusCode::BAD_REQUEST, 120 + Json(json!({"error": "InvalidToken", "message": "token is required"})), 121 + ) 122 + .into_response(); 123 + } 124 + 125 + if password.is_empty() { 126 + return ( 127 + StatusCode::BAD_REQUEST, 128 + Json(json!({"error": "InvalidRequest", "message": "password is required"})), 129 + ) 130 + .into_response(); 131 + } 132 + 133 + let user = sqlx::query!( 134 + "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 135 + token 136 + ) 137 + .fetch_optional(&state.db) 138 + .await; 139 + 140 + let (user_id, expires_at) = match user { 141 + Ok(Some(row)) => { 142 + let expires = row.password_reset_code_expires_at; 143 + (row.id, expires) 144 + } 145 + Ok(None) => { 146 + return ( 147 + StatusCode::BAD_REQUEST, 148 + Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 149 + ) 150 + .into_response(); 151 + } 152 + Err(e) => { 153 + error!("DB error in reset_password: {:?}", e); 154 + return ( 155 + StatusCode::INTERNAL_SERVER_ERROR, 156 + Json(json!({"error": "InternalError"})), 157 + ) 158 + .into_response(); 159 + } 160 + }; 161 + 162 + if let Some(exp) = expires_at { 163 + if Utc::now() > exp { 164 + let _ = sqlx::query!( 165 + "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 166 + user_id 167 + ) 168 + .execute(&state.db) 169 + .await; 170 + 171 + return ( 172 + StatusCode::BAD_REQUEST, 173 + Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 174 + ) 175 + .into_response(); 176 + } 177 + } else { 178 + return ( 179 + StatusCode::BAD_REQUEST, 180 + Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 181 + ) 182 + .into_response(); 183 + } 184 + 185 + let password_hash = match hash(password, DEFAULT_COST) { 186 + Ok(h) => h, 187 + Err(e) => { 188 + error!("Failed to hash password: {:?}", e); 189 + return ( 190 + StatusCode::INTERNAL_SERVER_ERROR, 191 + Json(json!({"error": "InternalError"})), 192 + ) 193 + .into_response(); 194 + } 195 + }; 196 + 197 + let update = sqlx::query!( 198 + "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2", 199 + password_hash, 200 + user_id 201 + ) 202 + .execute(&state.db) 203 + .await; 204 + 205 + if let Err(e) = update { 206 + error!("DB error updating password: {:?}", e); 207 + return ( 208 + StatusCode::INTERNAL_SERVER_ERROR, 209 + Json(json!({"error": "InternalError"})), 210 + ) 211 + .into_response(); 212 + } 213 + 214 + let _ = sqlx::query!("DELETE FROM sessions WHERE did = (SELECT did FROM users WHERE id = $1)", user_id) 215 + .execute(&state.db) 216 + .await; 217 + 218 + info!("Password reset completed for user {}", user_id); 219 + 220 + (StatusCode::OK, Json(json!({}))).into_response() 221 + }
+1 -1217
src/api/server/session.rs
··· 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7 }; 8 - use bcrypt::{hash, verify, DEFAULT_COST}; 9 - use chrono::{Duration, Utc}; 10 - use rand::Rng; 11 use serde::{Deserialize, Serialize}; 12 use serde_json::json; 13 use tracing::{error, info, warn}; 14 - use uuid::Uuid; 15 16 #[derive(Deserialize)] 17 pub struct GetServiceAuthParams { ··· 343 .into_response() 344 } 345 346 - pub async fn request_account_delete( 347 - State(state): State<AppState>, 348 - headers: axum::http::HeaderMap, 349 - ) -> Response { 350 - let auth_header = headers.get("Authorization"); 351 - if auth_header.is_none() { 352 - return ( 353 - StatusCode::UNAUTHORIZED, 354 - Json(json!({"error": "AuthenticationRequired"})), 355 - ) 356 - .into_response(); 357 - } 358 - 359 - let token = auth_header 360 - .unwrap() 361 - .to_str() 362 - .unwrap_or("") 363 - .replace("Bearer ", ""); 364 - 365 - let session = sqlx::query!( 366 - r#" 367 - SELECT s.did, u.id as user_id, u.email, u.handle, k.key_bytes 368 - FROM sessions s 369 - JOIN users u ON s.did = u.did 370 - JOIN user_keys k ON u.id = k.user_id 371 - WHERE s.access_jwt = $1 372 - "#, 373 - token 374 - ) 375 - .fetch_optional(&state.db) 376 - .await; 377 - 378 - let (did, user_id, email, handle, key_bytes) = match session { 379 - Ok(Some(row)) => (row.did, row.user_id, row.email, row.handle, row.key_bytes), 380 - Ok(None) => { 381 - return ( 382 - StatusCode::UNAUTHORIZED, 383 - Json(json!({"error": "AuthenticationFailed"})), 384 - ) 385 - .into_response(); 386 - } 387 - Err(e) => { 388 - error!("DB error in request_account_delete: {:?}", e); 389 - return ( 390 - StatusCode::INTERNAL_SERVER_ERROR, 391 - Json(json!({"error": "InternalError"})), 392 - ) 393 - .into_response(); 394 - } 395 - }; 396 - 397 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 398 - return ( 399 - StatusCode::UNAUTHORIZED, 400 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 401 - ) 402 - .into_response(); 403 - } 404 - 405 - let confirmation_token = Uuid::new_v4().to_string(); 406 - let expires_at = Utc::now() + Duration::minutes(15); 407 - 408 - let insert = sqlx::query!( 409 - "INSERT INTO account_deletion_requests (token, did, expires_at) VALUES ($1, $2, $3)", 410 - confirmation_token, 411 - did, 412 - expires_at 413 - ) 414 - .execute(&state.db) 415 - .await; 416 - 417 - if let Err(e) = insert { 418 - error!("DB error creating deletion token: {:?}", e); 419 - return ( 420 - StatusCode::INTERNAL_SERVER_ERROR, 421 - Json(json!({"error": "InternalError"})), 422 - ) 423 - .into_response(); 424 - } 425 - 426 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 427 - if let Err(e) = crate::notifications::enqueue_account_deletion( 428 - &state.db, 429 - user_id, 430 - &email, 431 - &handle, 432 - &confirmation_token, 433 - &hostname, 434 - ) 435 - .await 436 - { 437 - warn!("Failed to enqueue account deletion notification: {:?}", e); 438 - } 439 - 440 - info!("Account deletion requested for user {}", did); 441 - 442 - (StatusCode::OK, Json(json!({}))).into_response() 443 - } 444 - 445 pub async fn refresh_session( 446 State(state): State<AppState>, 447 headers: axum::http::HeaderMap, ··· 573 } 574 } 575 } 576 - 577 - #[derive(Serialize)] 578 - #[serde(rename_all = "camelCase")] 579 - pub struct CheckAccountStatusOutput { 580 - pub activated: bool, 581 - pub valid_did: bool, 582 - pub repo_commit: String, 583 - pub repo_rev: String, 584 - pub repo_blocks: i64, 585 - pub indexed_records: i64, 586 - pub private_state_values: i64, 587 - pub expected_blobs: i64, 588 - pub imported_blobs: i64, 589 - } 590 - 591 - pub async fn check_account_status( 592 - State(state): State<AppState>, 593 - headers: axum::http::HeaderMap, 594 - ) -> Response { 595 - let auth_header = headers.get("Authorization"); 596 - if auth_header.is_none() { 597 - return ( 598 - StatusCode::UNAUTHORIZED, 599 - Json(json!({"error": "AuthenticationRequired"})), 600 - ) 601 - .into_response(); 602 - } 603 - 604 - let token = auth_header 605 - .unwrap() 606 - .to_str() 607 - .unwrap_or("") 608 - .replace("Bearer ", ""); 609 - 610 - let session = sqlx::query!( 611 - r#" 612 - SELECT s.did, k.key_bytes, u.id as user_id 613 - FROM sessions s 614 - JOIN users u ON s.did = u.did 615 - JOIN user_keys k ON u.id = k.user_id 616 - WHERE s.access_jwt = $1 617 - "#, 618 - token 619 - ) 620 - .fetch_optional(&state.db) 621 - .await; 622 - 623 - let (did, key_bytes, user_id) = match session { 624 - Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 625 - Ok(None) => { 626 - return ( 627 - StatusCode::UNAUTHORIZED, 628 - Json(json!({"error": "AuthenticationFailed"})), 629 - ) 630 - .into_response(); 631 - } 632 - Err(e) => { 633 - error!("DB error in check_account_status: {:?}", e); 634 - return ( 635 - StatusCode::INTERNAL_SERVER_ERROR, 636 - Json(json!({"error": "InternalError"})), 637 - ) 638 - .into_response(); 639 - } 640 - }; 641 - 642 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 643 - return ( 644 - StatusCode::UNAUTHORIZED, 645 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 646 - ) 647 - .into_response(); 648 - } 649 - 650 - let user_status = sqlx::query!("SELECT deactivated_at FROM users WHERE did = $1", did) 651 - .fetch_optional(&state.db) 652 - .await; 653 - 654 - let deactivated_at = match user_status { 655 - Ok(Some(row)) => row.deactivated_at, 656 - _ => None, 657 - }; 658 - 659 - let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id) 660 - .fetch_optional(&state.db) 661 - .await; 662 - 663 - let repo_commit = match repo_result { 664 - Ok(Some(row)) => row.repo_root_cid, 665 - _ => String::new(), 666 - }; 667 - 668 - let record_count: i64 = sqlx::query_scalar!("SELECT COUNT(*) FROM records WHERE repo_id = $1", user_id) 669 - .fetch_one(&state.db) 670 - .await 671 - .unwrap_or(Some(0)) 672 - .unwrap_or(0); 673 - 674 - let blob_count: i64 = 675 - sqlx::query_scalar!("SELECT COUNT(*) FROM blobs WHERE created_by_user = $1", user_id) 676 - .fetch_one(&state.db) 677 - .await 678 - .unwrap_or(Some(0)) 679 - .unwrap_or(0); 680 - 681 - let valid_did = did.starts_with("did:"); 682 - 683 - ( 684 - StatusCode::OK, 685 - Json(CheckAccountStatusOutput { 686 - activated: deactivated_at.is_none(), 687 - valid_did, 688 - repo_commit: repo_commit.clone(), 689 - repo_rev: chrono::Utc::now().timestamp_millis().to_string(), 690 - repo_blocks: 0, 691 - indexed_records: record_count, 692 - private_state_values: 0, 693 - expected_blobs: blob_count, 694 - imported_blobs: blob_count, 695 - }), 696 - ) 697 - .into_response() 698 - } 699 - 700 - pub async fn activate_account( 701 - State(state): State<AppState>, 702 - headers: axum::http::HeaderMap, 703 - ) -> Response { 704 - let auth_header = headers.get("Authorization"); 705 - if auth_header.is_none() { 706 - return ( 707 - StatusCode::UNAUTHORIZED, 708 - Json(json!({"error": "AuthenticationRequired"})), 709 - ) 710 - .into_response(); 711 - } 712 - 713 - let token = auth_header 714 - .unwrap() 715 - .to_str() 716 - .unwrap_or("") 717 - .replace("Bearer ", ""); 718 - 719 - let session = sqlx::query!( 720 - r#" 721 - SELECT s.did, k.key_bytes 722 - FROM sessions s 723 - JOIN users u ON s.did = u.did 724 - JOIN user_keys k ON u.id = k.user_id 725 - WHERE s.access_jwt = $1 726 - "#, 727 - token 728 - ) 729 - .fetch_optional(&state.db) 730 - .await; 731 - 732 - let (did, key_bytes) = match session { 733 - Ok(Some(row)) => (row.did, row.key_bytes), 734 - Ok(None) => { 735 - return ( 736 - StatusCode::UNAUTHORIZED, 737 - Json(json!({"error": "AuthenticationFailed"})), 738 - ) 739 - .into_response(); 740 - } 741 - Err(e) => { 742 - error!("DB error in activate_account: {:?}", e); 743 - return ( 744 - StatusCode::INTERNAL_SERVER_ERROR, 745 - Json(json!({"error": "InternalError"})), 746 - ) 747 - .into_response(); 748 - } 749 - }; 750 - 751 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 752 - return ( 753 - StatusCode::UNAUTHORIZED, 754 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 755 - ) 756 - .into_response(); 757 - } 758 - 759 - let result = sqlx::query!("UPDATE users SET deactivated_at = NULL WHERE did = $1", did) 760 - .execute(&state.db) 761 - .await; 762 - 763 - match result { 764 - Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 765 - Err(e) => { 766 - error!("DB error activating account: {:?}", e); 767 - ( 768 - StatusCode::INTERNAL_SERVER_ERROR, 769 - Json(json!({"error": "InternalError"})), 770 - ) 771 - .into_response() 772 - } 773 - } 774 - } 775 - 776 - #[derive(Deserialize)] 777 - #[serde(rename_all = "camelCase")] 778 - pub struct DeactivateAccountInput { 779 - pub delete_after: Option<String>, 780 - } 781 - 782 - pub async fn deactivate_account( 783 - State(state): State<AppState>, 784 - headers: axum::http::HeaderMap, 785 - Json(_input): Json<DeactivateAccountInput>, 786 - ) -> Response { 787 - let auth_header = headers.get("Authorization"); 788 - if auth_header.is_none() { 789 - return ( 790 - StatusCode::UNAUTHORIZED, 791 - Json(json!({"error": "AuthenticationRequired"})), 792 - ) 793 - .into_response(); 794 - } 795 - 796 - let token = auth_header 797 - .unwrap() 798 - .to_str() 799 - .unwrap_or("") 800 - .replace("Bearer ", ""); 801 - 802 - let session = sqlx::query!( 803 - r#" 804 - SELECT s.did, k.key_bytes 805 - FROM sessions s 806 - JOIN users u ON s.did = u.did 807 - JOIN user_keys k ON u.id = k.user_id 808 - WHERE s.access_jwt = $1 809 - "#, 810 - token 811 - ) 812 - .fetch_optional(&state.db) 813 - .await; 814 - 815 - let (did, key_bytes) = match session { 816 - Ok(Some(row)) => (row.did, row.key_bytes), 817 - Ok(None) => { 818 - return ( 819 - StatusCode::UNAUTHORIZED, 820 - Json(json!({"error": "AuthenticationFailed"})), 821 - ) 822 - .into_response(); 823 - } 824 - Err(e) => { 825 - error!("DB error in deactivate_account: {:?}", e); 826 - return ( 827 - StatusCode::INTERNAL_SERVER_ERROR, 828 - Json(json!({"error": "InternalError"})), 829 - ) 830 - .into_response(); 831 - } 832 - }; 833 - 834 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 835 - return ( 836 - StatusCode::UNAUTHORIZED, 837 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 838 - ) 839 - .into_response(); 840 - } 841 - 842 - let result = sqlx::query!("UPDATE users SET deactivated_at = NOW() WHERE did = $1", did) 843 - .execute(&state.db) 844 - .await; 845 - 846 - match result { 847 - Ok(_) => (StatusCode::OK, Json(json!({}))).into_response(), 848 - Err(e) => { 849 - error!("DB error deactivating account: {:?}", e); 850 - ( 851 - StatusCode::INTERNAL_SERVER_ERROR, 852 - Json(json!({"error": "InternalError"})), 853 - ) 854 - .into_response() 855 - } 856 - } 857 - } 858 - 859 - #[derive(Serialize)] 860 - #[serde(rename_all = "camelCase")] 861 - pub struct AppPassword { 862 - pub name: String, 863 - pub created_at: String, 864 - pub privileged: bool, 865 - } 866 - 867 - #[derive(Serialize)] 868 - pub struct ListAppPasswordsOutput { 869 - pub passwords: Vec<AppPassword>, 870 - } 871 - 872 - pub async fn list_app_passwords( 873 - State(state): State<AppState>, 874 - headers: axum::http::HeaderMap, 875 - ) -> Response { 876 - let auth_header = headers.get("Authorization"); 877 - if auth_header.is_none() { 878 - return ( 879 - StatusCode::UNAUTHORIZED, 880 - Json(json!({"error": "AuthenticationRequired"})), 881 - ) 882 - .into_response(); 883 - } 884 - 885 - let token = auth_header 886 - .unwrap() 887 - .to_str() 888 - .unwrap_or("") 889 - .replace("Bearer ", ""); 890 - 891 - let session = sqlx::query!( 892 - r#" 893 - SELECT s.did, k.key_bytes, u.id as user_id 894 - FROM sessions s 895 - JOIN users u ON s.did = u.did 896 - JOIN user_keys k ON u.id = k.user_id 897 - WHERE s.access_jwt = $1 898 - "#, 899 - token 900 - ) 901 - .fetch_optional(&state.db) 902 - .await; 903 - 904 - let (_did, key_bytes, user_id) = match session { 905 - Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 906 - Ok(None) => { 907 - return ( 908 - StatusCode::UNAUTHORIZED, 909 - Json(json!({"error": "AuthenticationFailed"})), 910 - ) 911 - .into_response(); 912 - } 913 - Err(e) => { 914 - error!("DB error in list_app_passwords: {:?}", e); 915 - return ( 916 - StatusCode::INTERNAL_SERVER_ERROR, 917 - Json(json!({"error": "InternalError"})), 918 - ) 919 - .into_response(); 920 - } 921 - }; 922 - 923 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 924 - return ( 925 - StatusCode::UNAUTHORIZED, 926 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 927 - ) 928 - .into_response(); 929 - } 930 - 931 - let result = sqlx::query!("SELECT name, created_at, privileged FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC", user_id) 932 - .fetch_all(&state.db) 933 - .await; 934 - 935 - match result { 936 - Ok(rows) => { 937 - let passwords: Vec<AppPassword> = rows 938 - .iter() 939 - .map(|row| { 940 - AppPassword { 941 - name: row.name.clone(), 942 - created_at: row.created_at.to_rfc3339(), 943 - privileged: row.privileged, 944 - } 945 - }) 946 - .collect(); 947 - 948 - (StatusCode::OK, Json(ListAppPasswordsOutput { passwords })).into_response() 949 - } 950 - Err(e) => { 951 - error!("DB error listing app passwords: {:?}", e); 952 - ( 953 - StatusCode::INTERNAL_SERVER_ERROR, 954 - Json(json!({"error": "InternalError"})), 955 - ) 956 - .into_response() 957 - } 958 - } 959 - } 960 - 961 - #[derive(Deserialize)] 962 - pub struct CreateAppPasswordInput { 963 - pub name: String, 964 - pub privileged: Option<bool>, 965 - } 966 - 967 - #[derive(Serialize)] 968 - #[serde(rename_all = "camelCase")] 969 - pub struct CreateAppPasswordOutput { 970 - pub name: String, 971 - pub password: String, 972 - pub created_at: String, 973 - pub privileged: bool, 974 - } 975 - 976 - pub async fn create_app_password( 977 - State(state): State<AppState>, 978 - headers: axum::http::HeaderMap, 979 - Json(input): Json<CreateAppPasswordInput>, 980 - ) -> Response { 981 - let auth_header = headers.get("Authorization"); 982 - if auth_header.is_none() { 983 - return ( 984 - StatusCode::UNAUTHORIZED, 985 - Json(json!({"error": "AuthenticationRequired"})), 986 - ) 987 - .into_response(); 988 - } 989 - 990 - let token = auth_header 991 - .unwrap() 992 - .to_str() 993 - .unwrap_or("") 994 - .replace("Bearer ", ""); 995 - 996 - let session = sqlx::query!( 997 - r#" 998 - SELECT s.did, k.key_bytes, u.id as user_id 999 - FROM sessions s 1000 - JOIN users u ON s.did = u.did 1001 - JOIN user_keys k ON u.id = k.user_id 1002 - WHERE s.access_jwt = $1 1003 - "#, 1004 - token 1005 - ) 1006 - .fetch_optional(&state.db) 1007 - .await; 1008 - 1009 - let (_did, key_bytes, user_id) = match session { 1010 - Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 1011 - Ok(None) => { 1012 - return ( 1013 - StatusCode::UNAUTHORIZED, 1014 - Json(json!({"error": "AuthenticationFailed"})), 1015 - ) 1016 - .into_response(); 1017 - } 1018 - Err(e) => { 1019 - error!("DB error in create_app_password: {:?}", e); 1020 - return ( 1021 - StatusCode::INTERNAL_SERVER_ERROR, 1022 - Json(json!({"error": "InternalError"})), 1023 - ) 1024 - .into_response(); 1025 - } 1026 - }; 1027 - 1028 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 1029 - return ( 1030 - StatusCode::UNAUTHORIZED, 1031 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 1032 - ) 1033 - .into_response(); 1034 - } 1035 - 1036 - let name = input.name.trim(); 1037 - if name.is_empty() { 1038 - return ( 1039 - StatusCode::BAD_REQUEST, 1040 - Json(json!({"error": "InvalidRequest", "message": "name is required"})), 1041 - ) 1042 - .into_response(); 1043 - } 1044 - 1045 - let existing = sqlx::query!("SELECT id FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 1046 - .fetch_optional(&state.db) 1047 - .await; 1048 - 1049 - if let Ok(Some(_)) = existing { 1050 - return ( 1051 - StatusCode::BAD_REQUEST, 1052 - Json(json!({"error": "DuplicateAppPassword", "message": "App password with this name already exists"})), 1053 - ) 1054 - .into_response(); 1055 - } 1056 - 1057 - let password: String = (0..4) 1058 - .map(|_| { 1059 - use rand::Rng; 1060 - let mut rng = rand::thread_rng(); 1061 - let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 1062 - (0..4).map(|_| chars[rng.gen_range(0..chars.len())]).collect::<String>() 1063 - }) 1064 - .collect::<Vec<String>>() 1065 - .join("-"); 1066 - 1067 - let password_hash = match bcrypt::hash(&password, bcrypt::DEFAULT_COST) { 1068 - Ok(h) => h, 1069 - Err(e) => { 1070 - error!("Failed to hash password: {:?}", e); 1071 - return ( 1072 - StatusCode::INTERNAL_SERVER_ERROR, 1073 - Json(json!({"error": "InternalError"})), 1074 - ) 1075 - .into_response(); 1076 - } 1077 - }; 1078 - 1079 - let privileged = input.privileged.unwrap_or(false); 1080 - let created_at = chrono::Utc::now(); 1081 - 1082 - let result = sqlx::query!( 1083 - "INSERT INTO app_passwords (user_id, name, password_hash, created_at, privileged) VALUES ($1, $2, $3, $4, $5)", 1084 - user_id, 1085 - name, 1086 - password_hash, 1087 - created_at, 1088 - privileged 1089 - ) 1090 - .execute(&state.db) 1091 - .await; 1092 - 1093 - match result { 1094 - Ok(_) => ( 1095 - StatusCode::OK, 1096 - Json(CreateAppPasswordOutput { 1097 - name: name.to_string(), 1098 - password, 1099 - created_at: created_at.to_rfc3339(), 1100 - privileged, 1101 - }), 1102 - ) 1103 - .into_response(), 1104 - Err(e) => { 1105 - error!("DB error creating app password: {:?}", e); 1106 - ( 1107 - StatusCode::INTERNAL_SERVER_ERROR, 1108 - Json(json!({"error": "InternalError"})), 1109 - ) 1110 - .into_response() 1111 - } 1112 - } 1113 - } 1114 - 1115 - #[derive(Deserialize)] 1116 - pub struct RevokeAppPasswordInput { 1117 - pub name: String, 1118 - } 1119 - 1120 - pub async fn revoke_app_password( 1121 - State(state): State<AppState>, 1122 - headers: axum::http::HeaderMap, 1123 - Json(input): Json<RevokeAppPasswordInput>, 1124 - ) -> Response { 1125 - let auth_header = headers.get("Authorization"); 1126 - if auth_header.is_none() { 1127 - return ( 1128 - StatusCode::UNAUTHORIZED, 1129 - Json(json!({"error": "AuthenticationRequired"})), 1130 - ) 1131 - .into_response(); 1132 - } 1133 - 1134 - let token = auth_header 1135 - .unwrap() 1136 - .to_str() 1137 - .unwrap_or("") 1138 - .replace("Bearer ", ""); 1139 - 1140 - let session = sqlx::query!( 1141 - r#" 1142 - SELECT s.did, k.key_bytes, u.id as user_id 1143 - FROM sessions s 1144 - JOIN users u ON s.did = u.did 1145 - JOIN user_keys k ON u.id = k.user_id 1146 - WHERE s.access_jwt = $1 1147 - "#, 1148 - token 1149 - ) 1150 - .fetch_optional(&state.db) 1151 - .await; 1152 - 1153 - let (_did, key_bytes, user_id) = match session { 1154 - Ok(Some(row)) => (row.did, row.key_bytes, row.user_id), 1155 - Ok(None) => { 1156 - return ( 1157 - StatusCode::UNAUTHORIZED, 1158 - Json(json!({"error": "AuthenticationFailed"})), 1159 - ) 1160 - .into_response(); 1161 - } 1162 - Err(e) => { 1163 - error!("DB error in revoke_app_password: {:?}", e); 1164 - return ( 1165 - StatusCode::INTERNAL_SERVER_ERROR, 1166 - Json(json!({"error": "InternalError"})), 1167 - ) 1168 - .into_response(); 1169 - } 1170 - }; 1171 - 1172 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 1173 - return ( 1174 - StatusCode::UNAUTHORIZED, 1175 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 1176 - ) 1177 - .into_response(); 1178 - } 1179 - 1180 - let name = input.name.trim(); 1181 - if name.is_empty() { 1182 - return ( 1183 - StatusCode::BAD_REQUEST, 1184 - Json(json!({"error": "InvalidRequest", "message": "name is required"})), 1185 - ) 1186 - .into_response(); 1187 - } 1188 - 1189 - let result = sqlx::query!("DELETE FROM app_passwords WHERE user_id = $1 AND name = $2", user_id, name) 1190 - .execute(&state.db) 1191 - .await; 1192 - 1193 - match result { 1194 - Ok(r) => { 1195 - if r.rows_affected() == 0 { 1196 - return ( 1197 - StatusCode::NOT_FOUND, 1198 - Json(json!({"error": "AppPasswordNotFound", "message": "App password not found"})), 1199 - ) 1200 - .into_response(); 1201 - } 1202 - (StatusCode::OK, Json(json!({}))).into_response() 1203 - } 1204 - Err(e) => { 1205 - error!("DB error revoking app password: {:?}", e); 1206 - ( 1207 - StatusCode::INTERNAL_SERVER_ERROR, 1208 - Json(json!({"error": "InternalError"})), 1209 - ) 1210 - .into_response() 1211 - } 1212 - } 1213 - } 1214 - 1215 - fn generate_reset_code() -> String { 1216 - let mut rng = rand::thread_rng(); 1217 - let chars: Vec<char> = "abcdefghijklmnopqrstuvwxyz234567".chars().collect(); 1218 - let part1: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); 1219 - let part2: String = (0..5).map(|_| chars[rng.gen_range(0..chars.len())]).collect(); 1220 - format!("{}-{}", part1, part2) 1221 - } 1222 - 1223 - #[derive(Deserialize)] 1224 - pub struct RequestPasswordResetInput { 1225 - pub email: String, 1226 - } 1227 - 1228 - pub async fn request_password_reset( 1229 - State(state): State<AppState>, 1230 - Json(input): Json<RequestPasswordResetInput>, 1231 - ) -> Response { 1232 - let email = input.email.trim().to_lowercase(); 1233 - if email.is_empty() { 1234 - return ( 1235 - StatusCode::BAD_REQUEST, 1236 - Json(json!({"error": "InvalidRequest", "message": "email is required"})), 1237 - ) 1238 - .into_response(); 1239 - } 1240 - 1241 - let user = sqlx::query!( 1242 - "SELECT id, handle FROM users WHERE LOWER(email) = $1", 1243 - email 1244 - ) 1245 - .fetch_optional(&state.db) 1246 - .await; 1247 - 1248 - let (user_id, handle) = match user { 1249 - Ok(Some(row)) => (row.id, row.handle), 1250 - Ok(None) => { 1251 - info!("Password reset requested for unknown email: {}", email); 1252 - return (StatusCode::OK, Json(json!({}))).into_response(); 1253 - } 1254 - Err(e) => { 1255 - error!("DB error in request_password_reset: {:?}", e); 1256 - return ( 1257 - StatusCode::INTERNAL_SERVER_ERROR, 1258 - Json(json!({"error": "InternalError"})), 1259 - ) 1260 - .into_response(); 1261 - } 1262 - }; 1263 - 1264 - let code = generate_reset_code(); 1265 - let expires_at = Utc::now() + Duration::minutes(10); 1266 - 1267 - let update = sqlx::query!( 1268 - "UPDATE users SET password_reset_code = $1, password_reset_code_expires_at = $2 WHERE id = $3", 1269 - code, 1270 - expires_at, 1271 - user_id 1272 - ) 1273 - .execute(&state.db) 1274 - .await; 1275 - 1276 - if let Err(e) = update { 1277 - error!("DB error setting reset code: {:?}", e); 1278 - return ( 1279 - StatusCode::INTERNAL_SERVER_ERROR, 1280 - Json(json!({"error": "InternalError"})), 1281 - ) 1282 - .into_response(); 1283 - } 1284 - 1285 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1286 - if let Err(e) = crate::notifications::enqueue_password_reset( 1287 - &state.db, 1288 - user_id, 1289 - &email, 1290 - &handle, 1291 - &code, 1292 - &hostname, 1293 - ) 1294 - .await 1295 - { 1296 - warn!("Failed to enqueue password reset notification: {:?}", e); 1297 - } 1298 - 1299 - info!("Password reset requested for user {}", user_id); 1300 - 1301 - (StatusCode::OK, Json(json!({}))).into_response() 1302 - } 1303 - 1304 - #[derive(Deserialize)] 1305 - pub struct ResetPasswordInput { 1306 - pub token: String, 1307 - pub password: String, 1308 - } 1309 - 1310 - pub async fn reset_password( 1311 - State(state): State<AppState>, 1312 - Json(input): Json<ResetPasswordInput>, 1313 - ) -> Response { 1314 - let token = input.token.trim(); 1315 - let password = &input.password; 1316 - 1317 - if token.is_empty() { 1318 - return ( 1319 - StatusCode::BAD_REQUEST, 1320 - Json(json!({"error": "InvalidToken", "message": "token is required"})), 1321 - ) 1322 - .into_response(); 1323 - } 1324 - 1325 - if password.is_empty() { 1326 - return ( 1327 - StatusCode::BAD_REQUEST, 1328 - Json(json!({"error": "InvalidRequest", "message": "password is required"})), 1329 - ) 1330 - .into_response(); 1331 - } 1332 - 1333 - let user = sqlx::query!( 1334 - "SELECT id, password_reset_code, password_reset_code_expires_at FROM users WHERE password_reset_code = $1", 1335 - token 1336 - ) 1337 - .fetch_optional(&state.db) 1338 - .await; 1339 - 1340 - let (user_id, expires_at) = match user { 1341 - Ok(Some(row)) => { 1342 - let expires = row.password_reset_code_expires_at; 1343 - (row.id, expires) 1344 - } 1345 - Ok(None) => { 1346 - return ( 1347 - StatusCode::BAD_REQUEST, 1348 - Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 1349 - ) 1350 - .into_response(); 1351 - } 1352 - Err(e) => { 1353 - error!("DB error in reset_password: {:?}", e); 1354 - return ( 1355 - StatusCode::INTERNAL_SERVER_ERROR, 1356 - Json(json!({"error": "InternalError"})), 1357 - ) 1358 - .into_response(); 1359 - } 1360 - }; 1361 - 1362 - if let Some(exp) = expires_at { 1363 - if Utc::now() > exp { 1364 - let _ = sqlx::query!( 1365 - "UPDATE users SET password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $1", 1366 - user_id 1367 - ) 1368 - .execute(&state.db) 1369 - .await; 1370 - 1371 - return ( 1372 - StatusCode::BAD_REQUEST, 1373 - Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 1374 - ) 1375 - .into_response(); 1376 - } 1377 - } else { 1378 - return ( 1379 - StatusCode::BAD_REQUEST, 1380 - Json(json!({"error": "InvalidToken", "message": "Invalid or expired token"})), 1381 - ) 1382 - .into_response(); 1383 - } 1384 - 1385 - let password_hash = match hash(password, DEFAULT_COST) { 1386 - Ok(h) => h, 1387 - Err(e) => { 1388 - error!("Failed to hash password: {:?}", e); 1389 - return ( 1390 - StatusCode::INTERNAL_SERVER_ERROR, 1391 - Json(json!({"error": "InternalError"})), 1392 - ) 1393 - .into_response(); 1394 - } 1395 - }; 1396 - 1397 - let update = sqlx::query!( 1398 - "UPDATE users SET password_hash = $1, password_reset_code = NULL, password_reset_code_expires_at = NULL WHERE id = $2", 1399 - password_hash, 1400 - user_id 1401 - ) 1402 - .execute(&state.db) 1403 - .await; 1404 - 1405 - if let Err(e) = update { 1406 - error!("DB error updating password: {:?}", e); 1407 - return ( 1408 - StatusCode::INTERNAL_SERVER_ERROR, 1409 - Json(json!({"error": "InternalError"})), 1410 - ) 1411 - .into_response(); 1412 - } 1413 - 1414 - let _ = sqlx::query!("DELETE FROM sessions WHERE did = (SELECT did FROM users WHERE id = $1)", user_id) 1415 - .execute(&state.db) 1416 - .await; 1417 - 1418 - info!("Password reset completed for user {}", user_id); 1419 - 1420 - (StatusCode::OK, Json(json!({}))).into_response() 1421 - } 1422 - 1423 - #[derive(Deserialize)] 1424 - #[serde(rename_all = "camelCase")] 1425 - pub struct RequestEmailUpdateInput { 1426 - pub email: String, 1427 - } 1428 - 1429 - pub async fn request_email_update( 1430 - State(state): State<AppState>, 1431 - headers: axum::http::HeaderMap, 1432 - Json(input): Json<RequestEmailUpdateInput>, 1433 - ) -> Response { 1434 - let auth_header = headers.get("Authorization"); 1435 - if auth_header.is_none() { 1436 - return ( 1437 - StatusCode::UNAUTHORIZED, 1438 - Json(json!({"error": "AuthenticationRequired"})), 1439 - ) 1440 - .into_response(); 1441 - } 1442 - 1443 - let token = auth_header 1444 - .unwrap() 1445 - .to_str() 1446 - .unwrap_or("") 1447 - .replace("Bearer ", ""); 1448 - 1449 - let session = sqlx::query!( 1450 - r#" 1451 - SELECT s.did, k.key_bytes, u.id as user_id, u.handle 1452 - FROM sessions s 1453 - JOIN users u ON s.did = u.did 1454 - JOIN user_keys k ON u.id = k.user_id 1455 - WHERE s.access_jwt = $1 1456 - "#, 1457 - token 1458 - ) 1459 - .fetch_optional(&state.db) 1460 - .await; 1461 - 1462 - let (_did, key_bytes, user_id, handle) = match session { 1463 - Ok(Some(row)) => (row.did, row.key_bytes, row.user_id, row.handle), 1464 - Ok(None) => { 1465 - return ( 1466 - StatusCode::UNAUTHORIZED, 1467 - Json(json!({"error": "AuthenticationFailed"})), 1468 - ) 1469 - .into_response(); 1470 - } 1471 - Err(e) => { 1472 - error!("DB error in request_email_update: {:?}", e); 1473 - return ( 1474 - StatusCode::INTERNAL_SERVER_ERROR, 1475 - Json(json!({"error": "InternalError"})), 1476 - ) 1477 - .into_response(); 1478 - } 1479 - }; 1480 - 1481 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 1482 - return ( 1483 - StatusCode::UNAUTHORIZED, 1484 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 1485 - ) 1486 - .into_response(); 1487 - } 1488 - 1489 - let email = input.email.trim().to_lowercase(); 1490 - if email.is_empty() { 1491 - return ( 1492 - StatusCode::BAD_REQUEST, 1493 - Json(json!({"error": "InvalidRequest", "message": "email is required"})), 1494 - ) 1495 - .into_response(); 1496 - } 1497 - 1498 - let exists = sqlx::query!("SELECT 1 as one FROM users WHERE LOWER(email) = $1", email) 1499 - .fetch_optional(&state.db) 1500 - .await; 1501 - 1502 - if let Ok(Some(_)) = exists { 1503 - return ( 1504 - StatusCode::BAD_REQUEST, 1505 - Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 1506 - ) 1507 - .into_response(); 1508 - } 1509 - 1510 - let code = generate_reset_code(); 1511 - let expires_at = Utc::now() + Duration::minutes(10); 1512 - 1513 - let update = sqlx::query!( 1514 - "UPDATE users SET email_pending_verification = $1, email_confirmation_code = $2, email_confirmation_code_expires_at = $3 WHERE id = $4", 1515 - email, 1516 - code, 1517 - expires_at, 1518 - user_id 1519 - ) 1520 - .execute(&state.db) 1521 - .await; 1522 - 1523 - if let Err(e) = update { 1524 - error!("DB error setting email update code: {:?}", e); 1525 - return ( 1526 - StatusCode::INTERNAL_SERVER_ERROR, 1527 - Json(json!({"error": "InternalError"})), 1528 - ) 1529 - .into_response(); 1530 - } 1531 - 1532 - let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 1533 - if let Err(e) = crate::notifications::enqueue_email_update( 1534 - &state.db, 1535 - user_id, 1536 - &email, 1537 - &handle, 1538 - &code, 1539 - &hostname, 1540 - ) 1541 - .await 1542 - { 1543 - warn!("Failed to enqueue email update notification: {:?}", e); 1544 - } 1545 - 1546 - info!("Email update requested for user {}", user_id); 1547 - 1548 - (StatusCode::OK, Json(json!({ "tokenRequired": true }))).into_response() 1549 - } 1550 - 1551 - #[derive(Deserialize)] 1552 - #[serde(rename_all = "camelCase")] 1553 - pub struct ConfirmEmailInput { 1554 - pub email: String, 1555 - pub token: String, 1556 - } 1557 - 1558 - pub async fn confirm_email( 1559 - State(state): State<AppState>, 1560 - headers: axum::http::HeaderMap, 1561 - Json(input): Json<ConfirmEmailInput>, 1562 - ) -> Response { 1563 - let auth_header = headers.get("Authorization"); 1564 - if auth_header.is_none() { 1565 - return ( 1566 - StatusCode::UNAUTHORIZED, 1567 - Json(json!({"error": "AuthenticationRequired"})), 1568 - ) 1569 - .into_response(); 1570 - } 1571 - 1572 - let token = auth_header 1573 - .unwrap() 1574 - .to_str() 1575 - .unwrap_or("") 1576 - .replace("Bearer ", ""); 1577 - 1578 - let session = sqlx::query!( 1579 - r#" 1580 - SELECT s.did, k.key_bytes, u.id as user_id, u.email_confirmation_code, u.email_confirmation_code_expires_at, u.email_pending_verification 1581 - FROM sessions s 1582 - JOIN users u ON s.did = u.did 1583 - JOIN user_keys k ON u.id = k.user_id 1584 - WHERE s.access_jwt = $1 1585 - "#, 1586 - token 1587 - ) 1588 - .fetch_optional(&state.db) 1589 - .await; 1590 - 1591 - let (_did, key_bytes, user_id, stored_code, expires_at, email_pending_verification) = match session { 1592 - Ok(Some(row)) => ( 1593 - row.did, 1594 - row.key_bytes, 1595 - row.user_id, 1596 - row.email_confirmation_code, 1597 - row.email_confirmation_code_expires_at, 1598 - row.email_pending_verification, 1599 - ), 1600 - Ok(None) => { 1601 - return ( 1602 - StatusCode::UNAUTHORIZED, 1603 - Json(json!({"error": "AuthenticationFailed"})), 1604 - ) 1605 - .into_response(); 1606 - } 1607 - Err(e) => { 1608 - error!("DB error in confirm_email: {:?}", e); 1609 - return ( 1610 - StatusCode::INTERNAL_SERVER_ERROR, 1611 - Json(json!({"error": "InternalError"})), 1612 - ) 1613 - .into_response(); 1614 - } 1615 - }; 1616 - 1617 - if let Err(_) = crate::auth::verify_token(&token, &key_bytes) { 1618 - return ( 1619 - StatusCode::UNAUTHORIZED, 1620 - Json(json!({"error": "AuthenticationFailed", "message": "Invalid token signature"})), 1621 - ) 1622 - .into_response(); 1623 - } 1624 - 1625 - let email = input.email.trim().to_lowercase(); 1626 - let confirmation_code = input.token.trim(); 1627 - 1628 - if email_pending_verification.is_none() || stored_code.is_none() || expires_at.is_none() { 1629 - return ( 1630 - StatusCode::BAD_REQUEST, 1631 - Json(json!({"error": "InvalidRequest", "message": "No pending email update found"})), 1632 - ) 1633 - .into_response(); 1634 - } 1635 - 1636 - let email_pending_verification = email_pending_verification.unwrap(); 1637 - if email_pending_verification != email { 1638 - return ( 1639 - StatusCode::BAD_REQUEST, 1640 - Json(json!({"error": "InvalidRequest", "message": "Email does not match pending update"})), 1641 - ) 1642 - .into_response(); 1643 - } 1644 - 1645 - if stored_code.unwrap() != confirmation_code { 1646 - return ( 1647 - StatusCode::BAD_REQUEST, 1648 - Json(json!({"error": "InvalidToken", "message": "Invalid token"})), 1649 - ) 1650 - .into_response(); 1651 - } 1652 - 1653 - if Utc::now() > expires_at.unwrap() { 1654 - return ( 1655 - StatusCode::BAD_REQUEST, 1656 - Json(json!({"error": "ExpiredToken", "message": "Token has expired"})), 1657 - ) 1658 - .into_response(); 1659 - } 1660 - 1661 - let update = sqlx::query!( 1662 - "UPDATE users SET email = $1, email_pending_verification = NULL, email_confirmation_code = NULL, email_confirmation_code_expires_at = NULL WHERE id = $2", 1663 - email_pending_verification, 1664 - user_id 1665 - ) 1666 - .execute(&state.db) 1667 - .await; 1668 - 1669 - if let Err(e) = update { 1670 - error!("DB error finalizing email update: {:?}", e); 1671 - if e.as_database_error().map(|db_err| db_err.is_unique_violation()).unwrap_or(false) { 1672 - return ( 1673 - StatusCode::BAD_REQUEST, 1674 - Json(json!({"error": "EmailTaken", "message": "Email already taken"})), 1675 - ) 1676 - .into_response(); 1677 - } 1678 - 1679 - return ( 1680 - StatusCode::INTERNAL_SERVER_ERROR, 1681 - Json(json!({"error": "InternalError"})), 1682 - ) 1683 - .into_response(); 1684 - } 1685 - 1686 - info!("Email updated for user {}", user_id); 1687 - 1688 - (StatusCode::OK, Json(json!({}))).into_response() 1689 - }
··· 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7 }; 8 + use bcrypt::verify; 9 use serde::{Deserialize, Serialize}; 10 use serde_json::json; 11 use tracing::{error, info, warn}; 12 13 #[derive(Deserialize)] 14 pub struct GetServiceAuthParams { ··· 340 .into_response() 341 } 342 343 pub async fn refresh_session( 344 State(state): State<AppState>, 345 headers: axum::http::HeaderMap, ··· 471 } 472 } 473 }
+229
src/sync/blob.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + body::Body, 5 + extract::{Query, State}, 6 + http::StatusCode, 7 + http::header, 8 + response::{IntoResponse, Response}, 9 + }; 10 + use serde::{Deserialize, Serialize}; 11 + use serde_json::json; 12 + use tracing::error; 13 + 14 + #[derive(Deserialize)] 15 + pub struct GetBlobParams { 16 + pub did: String, 17 + pub cid: String, 18 + } 19 + 20 + pub async fn get_blob( 21 + State(state): State<AppState>, 22 + Query(params): Query<GetBlobParams>, 23 + ) -> Response { 24 + let did = params.did.trim(); 25 + let cid = params.cid.trim(); 26 + 27 + if did.is_empty() { 28 + return ( 29 + StatusCode::BAD_REQUEST, 30 + Json(json!({"error": "InvalidRequest", "message": "did is required"})), 31 + ) 32 + .into_response(); 33 + } 34 + 35 + if cid.is_empty() { 36 + return ( 37 + StatusCode::BAD_REQUEST, 38 + Json(json!({"error": "InvalidRequest", "message": "cid is required"})), 39 + ) 40 + .into_response(); 41 + } 42 + 43 + let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 44 + .fetch_optional(&state.db) 45 + .await; 46 + 47 + match user_exists { 48 + Ok(None) => { 49 + return ( 50 + StatusCode::NOT_FOUND, 51 + Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 52 + ) 53 + .into_response(); 54 + } 55 + Err(e) => { 56 + error!("DB error in get_blob: {:?}", e); 57 + return ( 58 + StatusCode::INTERNAL_SERVER_ERROR, 59 + Json(json!({"error": "InternalError"})), 60 + ) 61 + .into_response(); 62 + } 63 + Ok(Some(_)) => {} 64 + } 65 + 66 + let blob_result = sqlx::query!("SELECT storage_key, mime_type FROM blobs WHERE cid = $1", cid) 67 + .fetch_optional(&state.db) 68 + .await; 69 + 70 + match blob_result { 71 + Ok(Some(row)) => { 72 + let storage_key = &row.storage_key; 73 + let mime_type = &row.mime_type; 74 + 75 + match state.blob_store.get(&storage_key).await { 76 + Ok(data) => Response::builder() 77 + .status(StatusCode::OK) 78 + .header(header::CONTENT_TYPE, mime_type) 79 + .body(Body::from(data)) 80 + .unwrap(), 81 + Err(e) => { 82 + error!("Failed to fetch blob from storage: {:?}", e); 83 + ( 84 + StatusCode::NOT_FOUND, 85 + Json(json!({"error": "BlobNotFound", "message": "Blob not found in storage"})), 86 + ) 87 + .into_response() 88 + } 89 + } 90 + } 91 + Ok(None) => ( 92 + StatusCode::NOT_FOUND, 93 + Json(json!({"error": "BlobNotFound", "message": "Blob not found"})), 94 + ) 95 + .into_response(), 96 + Err(e) => { 97 + error!("DB error in get_blob: {:?}", e); 98 + ( 99 + StatusCode::INTERNAL_SERVER_ERROR, 100 + Json(json!({"error": "InternalError"})), 101 + ) 102 + .into_response() 103 + } 104 + } 105 + } 106 + 107 + #[derive(Deserialize)] 108 + pub struct ListBlobsParams { 109 + pub did: String, 110 + pub since: Option<String>, 111 + pub limit: Option<i64>, 112 + pub cursor: Option<String>, 113 + } 114 + 115 + #[derive(Serialize)] 116 + pub struct ListBlobsOutput { 117 + pub cursor: Option<String>, 118 + pub cids: Vec<String>, 119 + } 120 + 121 + pub async fn list_blobs( 122 + State(state): State<AppState>, 123 + Query(params): Query<ListBlobsParams>, 124 + ) -> Response { 125 + let did = params.did.trim(); 126 + 127 + if did.is_empty() { 128 + return ( 129 + StatusCode::BAD_REQUEST, 130 + Json(json!({"error": "InvalidRequest", "message": "did is required"})), 131 + ) 132 + .into_response(); 133 + } 134 + 135 + let limit = params.limit.unwrap_or(500).min(1000); 136 + let cursor_cid = params.cursor.as_deref().unwrap_or(""); 137 + 138 + let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 139 + .fetch_optional(&state.db) 140 + .await; 141 + 142 + let user_id = match user_result { 143 + Ok(Some(row)) => row.id, 144 + Ok(None) => { 145 + return ( 146 + StatusCode::NOT_FOUND, 147 + Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 148 + ) 149 + .into_response(); 150 + } 151 + Err(e) => { 152 + error!("DB error in list_blobs: {:?}", e); 153 + return ( 154 + StatusCode::INTERNAL_SERVER_ERROR, 155 + Json(json!({"error": "InternalError"})), 156 + ) 157 + .into_response(); 158 + } 159 + }; 160 + 161 + let cids_result: Result<Vec<String>, sqlx::Error> = if let Some(since) = &params.since { 162 + let since_time = chrono::DateTime::parse_from_rfc3339(since) 163 + .map(|dt| dt.with_timezone(&chrono::Utc)) 164 + .unwrap_or_else(|_| chrono::Utc::now()); 165 + sqlx::query!( 166 + r#" 167 + SELECT cid FROM blobs 168 + WHERE created_by_user = $1 AND cid > $2 AND created_at > $3 169 + ORDER BY cid ASC 170 + LIMIT $4 171 + "#, 172 + user_id, 173 + cursor_cid, 174 + since_time, 175 + limit + 1 176 + ) 177 + .fetch_all(&state.db) 178 + .await 179 + .map(|rows| rows.into_iter().map(|r| r.cid).collect()) 180 + } else { 181 + sqlx::query!( 182 + r#" 183 + SELECT cid FROM blobs 184 + WHERE created_by_user = $1 AND cid > $2 185 + ORDER BY cid ASC 186 + LIMIT $3 187 + "#, 188 + user_id, 189 + cursor_cid, 190 + limit + 1 191 + ) 192 + .fetch_all(&state.db) 193 + .await 194 + .map(|rows| rows.into_iter().map(|r| r.cid).collect()) 195 + }; 196 + 197 + match cids_result { 198 + Ok(cids) => { 199 + let has_more = cids.len() as i64 > limit; 200 + let cids: Vec<String> = cids 201 + .into_iter() 202 + .take(limit as usize) 203 + .collect(); 204 + 205 + let next_cursor = if has_more { 206 + cids.last().cloned() 207 + } else { 208 + None 209 + }; 210 + 211 + ( 212 + StatusCode::OK, 213 + Json(ListBlobsOutput { 214 + cursor: next_cursor, 215 + cids, 216 + }), 217 + ) 218 + .into_response() 219 + } 220 + Err(e) => { 221 + error!("DB error in list_blobs: {:?}", e); 222 + ( 223 + StatusCode::INTERNAL_SERVER_ERROR, 224 + Json(json!({"error": "InternalError"})), 225 + ) 226 + .into_response() 227 + } 228 + } 229 + }
+32
src/sync/car.rs
···
··· 1 + use cid::Cid; 2 + use std::io::Write; 3 + 4 + pub fn write_varint<W: Write>(mut writer: W, mut value: u64) -> std::io::Result<()> { 5 + loop { 6 + let mut byte = (value & 0x7F) as u8; 7 + value >>= 7; 8 + if value != 0 { 9 + byte |= 0x80; 10 + } 11 + writer.write_all(&[byte])?; 12 + if value == 0 { 13 + break; 14 + } 15 + } 16 + Ok(()) 17 + } 18 + 19 + pub fn ld_write<W: Write>(mut writer: W, data: &[u8]) -> std::io::Result<()> { 20 + write_varint(&mut writer, data.len() as u64)?; 21 + writer.write_all(data)?; 22 + Ok(()) 23 + } 24 + 25 + pub fn encode_car_header(root_cid: &Cid) -> Vec<u8> { 26 + let header = serde_ipld_dagcbor::to_vec(&serde_json::json!({ 27 + "version": 1u64, 28 + "roots": [root_cid.to_bytes()] 29 + })) 30 + .unwrap_or_default(); 31 + header 32 + }
+227
src/sync/commit.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Query, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use serde::{Deserialize, Serialize}; 9 + use serde_json::json; 10 + use tracing::error; 11 + 12 + #[derive(Deserialize)] 13 + pub struct GetLatestCommitParams { 14 + pub did: String, 15 + } 16 + 17 + #[derive(Serialize)] 18 + pub struct GetLatestCommitOutput { 19 + pub cid: String, 20 + pub rev: String, 21 + } 22 + 23 + pub async fn get_latest_commit( 24 + State(state): State<AppState>, 25 + Query(params): Query<GetLatestCommitParams>, 26 + ) -> Response { 27 + let did = params.did.trim(); 28 + 29 + if did.is_empty() { 30 + return ( 31 + StatusCode::BAD_REQUEST, 32 + Json(json!({"error": "InvalidRequest", "message": "did is required"})), 33 + ) 34 + .into_response(); 35 + } 36 + 37 + let result = sqlx::query!( 38 + r#" 39 + SELECT r.repo_root_cid 40 + FROM repos r 41 + JOIN users u ON r.user_id = u.id 42 + WHERE u.did = $1 43 + "#, 44 + did 45 + ) 46 + .fetch_optional(&state.db) 47 + .await; 48 + 49 + match result { 50 + Ok(Some(row)) => { 51 + ( 52 + StatusCode::OK, 53 + Json(GetLatestCommitOutput { 54 + cid: row.repo_root_cid, 55 + rev: chrono::Utc::now().timestamp_millis().to_string(), 56 + }), 57 + ) 58 + .into_response() 59 + } 60 + Ok(None) => ( 61 + StatusCode::NOT_FOUND, 62 + Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 63 + ) 64 + .into_response(), 65 + Err(e) => { 66 + error!("DB error in get_latest_commit: {:?}", e); 67 + ( 68 + StatusCode::INTERNAL_SERVER_ERROR, 69 + Json(json!({"error": "InternalError"})), 70 + ) 71 + .into_response() 72 + } 73 + } 74 + } 75 + 76 + #[derive(Deserialize)] 77 + pub struct ListReposParams { 78 + pub limit: Option<i64>, 79 + pub cursor: Option<String>, 80 + } 81 + 82 + #[derive(Serialize)] 83 + #[serde(rename_all = "camelCase")] 84 + pub struct RepoInfo { 85 + pub did: String, 86 + pub head: String, 87 + pub rev: String, 88 + pub active: bool, 89 + } 90 + 91 + #[derive(Serialize)] 92 + pub struct ListReposOutput { 93 + pub cursor: Option<String>, 94 + pub repos: Vec<RepoInfo>, 95 + } 96 + 97 + pub async fn list_repos( 98 + State(state): State<AppState>, 99 + Query(params): Query<ListReposParams>, 100 + ) -> Response { 101 + let limit = params.limit.unwrap_or(50).min(1000); 102 + let cursor_did = params.cursor.as_deref().unwrap_or(""); 103 + 104 + let result = sqlx::query!( 105 + r#" 106 + SELECT u.did, r.repo_root_cid 107 + FROM repos r 108 + JOIN users u ON r.user_id = u.id 109 + WHERE u.did > $1 110 + ORDER BY u.did ASC 111 + LIMIT $2 112 + "#, 113 + cursor_did, 114 + limit + 1 115 + ) 116 + .fetch_all(&state.db) 117 + .await; 118 + 119 + match result { 120 + Ok(rows) => { 121 + let has_more = rows.len() as i64 > limit; 122 + let repos: Vec<RepoInfo> = rows 123 + .iter() 124 + .take(limit as usize) 125 + .map(|row| { 126 + RepoInfo { 127 + did: row.did.clone(), 128 + head: row.repo_root_cid.clone(), 129 + rev: chrono::Utc::now().timestamp_millis().to_string(), 130 + active: true, 131 + } 132 + }) 133 + .collect(); 134 + 135 + let next_cursor = if has_more { 136 + repos.last().map(|r| r.did.clone()) 137 + } else { 138 + None 139 + }; 140 + 141 + ( 142 + StatusCode::OK, 143 + Json(ListReposOutput { 144 + cursor: next_cursor, 145 + repos, 146 + }), 147 + ) 148 + .into_response() 149 + } 150 + Err(e) => { 151 + error!("DB error in list_repos: {:?}", e); 152 + ( 153 + StatusCode::INTERNAL_SERVER_ERROR, 154 + Json(json!({"error": "InternalError"})), 155 + ) 156 + .into_response() 157 + } 158 + } 159 + } 160 + 161 + #[derive(Deserialize)] 162 + pub struct GetRepoStatusParams { 163 + pub did: String, 164 + } 165 + 166 + #[derive(Serialize)] 167 + pub struct GetRepoStatusOutput { 168 + pub did: String, 169 + pub active: bool, 170 + pub rev: Option<String>, 171 + } 172 + 173 + pub async fn get_repo_status( 174 + State(state): State<AppState>, 175 + Query(params): Query<GetRepoStatusParams>, 176 + ) -> Response { 177 + let did = params.did.trim(); 178 + 179 + if did.is_empty() { 180 + return ( 181 + StatusCode::BAD_REQUEST, 182 + Json(json!({"error": "InvalidRequest", "message": "did is required"})), 183 + ) 184 + .into_response(); 185 + } 186 + 187 + let result = sqlx::query!( 188 + r#" 189 + SELECT u.did, r.repo_root_cid 190 + FROM users u 191 + LEFT JOIN repos r ON u.id = r.user_id 192 + WHERE u.did = $1 193 + "#, 194 + did 195 + ) 196 + .fetch_optional(&state.db) 197 + .await; 198 + 199 + match result { 200 + Ok(Some(row)) => { 201 + let rev = Some(chrono::Utc::now().timestamp_millis().to_string()); 202 + 203 + ( 204 + StatusCode::OK, 205 + Json(GetRepoStatusOutput { 206 + did: row.did, 207 + active: true, 208 + rev, 209 + }), 210 + ) 211 + .into_response() 212 + } 213 + Ok(None) => ( 214 + StatusCode::NOT_FOUND, 215 + Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 216 + ) 217 + .into_response(), 218 + Err(e) => { 219 + error!("DB error in get_repo_status: {:?}", e); 220 + ( 221 + StatusCode::INTERNAL_SERVER_ERROR, 222 + Json(json!({"error": "InternalError"})), 223 + ) 224 + .into_response() 225 + } 226 + } 227 + }
+40
src/sync/crawl.rs
···
··· 1 + use crate::state::AppState; 2 + use axum::{ 3 + Json, 4 + extract::{Query, State}, 5 + http::StatusCode, 6 + response::{IntoResponse, Response}, 7 + }; 8 + use serde::Deserialize; 9 + use serde_json::json; 10 + use tracing::info; 11 + 12 + #[derive(Deserialize)] 13 + pub struct NotifyOfUpdateParams { 14 + pub hostname: String, 15 + } 16 + 17 + pub async fn notify_of_update( 18 + State(_state): State<AppState>, 19 + Query(params): Query<NotifyOfUpdateParams>, 20 + ) -> Response { 21 + info!("Received notifyOfUpdate from hostname: {}", params.hostname); 22 + info!("TODO: Queue job for notifyOfUpdate (not implemented)"); 23 + 24 + (StatusCode::OK, Json(json!({}))).into_response() 25 + } 26 + 27 + #[derive(Deserialize)] 28 + pub struct RequestCrawlInput { 29 + pub hostname: String, 30 + } 31 + 32 + pub async fn request_crawl( 33 + State(_state): State<AppState>, 34 + Json(input): Json<RequestCrawlInput>, 35 + ) -> Response { 36 + info!("Received requestCrawl for hostname: {}", input.hostname); 37 + info!("TODO: Queue job for requestCrawl (not implemented)"); 38 + 39 + (StatusCode::OK, Json(json!({}))).into_response() 40 + }
+9 -1050
src/sync/mod.rs
··· 1 - use crate::state::AppState; 2 - use axum::{ 3 - Json, 4 - body::Body, 5 - extract::{Query, State}, 6 - http::StatusCode, 7 - http::header, 8 - response::{IntoResponse, Response}, 9 - }; 10 - use bytes::Bytes; 11 - use cid::Cid; 12 - use jacquard_repo::{commit::Commit, storage::BlockStore}; 13 - use serde::{Deserialize, Serialize}; 14 - use serde_json::json; 15 - use std::collections::HashSet; 16 - use std::io::Write; 17 - use tracing::{error, info}; 18 - 19 - fn write_varint<W: Write>(mut writer: W, mut value: u64) -> std::io::Result<()> { 20 - loop { 21 - let mut byte = (value & 0x7F) as u8; 22 - value >>= 7; 23 - if value != 0 { 24 - byte |= 0x80; 25 - } 26 - writer.write_all(&[byte])?; 27 - if value == 0 { 28 - break; 29 - } 30 - } 31 - Ok(()) 32 - } 33 - 34 - fn ld_write<W: Write>(mut writer: W, data: &[u8]) -> std::io::Result<()> { 35 - write_varint(&mut writer, data.len() as u64)?; 36 - writer.write_all(data)?; 37 - Ok(()) 38 - } 39 - 40 - fn encode_car_header(root_cid: &Cid) -> Vec<u8> { 41 - let header = serde_ipld_dagcbor::to_vec(&serde_json::json!({ 42 - "version": 1u64, 43 - "roots": [root_cid.to_bytes()] 44 - })) 45 - .unwrap_or_default(); 46 - header 47 - } 48 - 49 - #[derive(Deserialize)] 50 - pub struct GetLatestCommitParams { 51 - pub did: String, 52 - } 53 - 54 - #[derive(Serialize)] 55 - pub struct GetLatestCommitOutput { 56 - pub cid: String, 57 - pub rev: String, 58 - } 59 - 60 - pub async fn get_latest_commit( 61 - State(state): State<AppState>, 62 - Query(params): Query<GetLatestCommitParams>, 63 - ) -> Response { 64 - let did = params.did.trim(); 65 - 66 - if did.is_empty() { 67 - return ( 68 - StatusCode::BAD_REQUEST, 69 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 70 - ) 71 - .into_response(); 72 - } 73 - 74 - let result = sqlx::query!( 75 - r#" 76 - SELECT r.repo_root_cid 77 - FROM repos r 78 - JOIN users u ON r.user_id = u.id 79 - WHERE u.did = $1 80 - "#, 81 - did 82 - ) 83 - .fetch_optional(&state.db) 84 - .await; 85 - 86 - match result { 87 - Ok(Some(row)) => { 88 - ( 89 - StatusCode::OK, 90 - Json(GetLatestCommitOutput { 91 - cid: row.repo_root_cid, 92 - rev: chrono::Utc::now().timestamp_millis().to_string(), 93 - }), 94 - ) 95 - .into_response() 96 - } 97 - Ok(None) => ( 98 - StatusCode::NOT_FOUND, 99 - Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 100 - ) 101 - .into_response(), 102 - Err(e) => { 103 - error!("DB error in get_latest_commit: {:?}", e); 104 - ( 105 - StatusCode::INTERNAL_SERVER_ERROR, 106 - Json(json!({"error": "InternalError"})), 107 - ) 108 - .into_response() 109 - } 110 - } 111 - } 112 - 113 - #[derive(Deserialize)] 114 - pub struct ListReposParams { 115 - pub limit: Option<i64>, 116 - pub cursor: Option<String>, 117 - } 118 - 119 - #[derive(Serialize)] 120 - #[serde(rename_all = "camelCase")] 121 - pub struct RepoInfo { 122 - pub did: String, 123 - pub head: String, 124 - pub rev: String, 125 - pub active: bool, 126 - } 127 - 128 - #[derive(Serialize)] 129 - pub struct ListReposOutput { 130 - pub cursor: Option<String>, 131 - pub repos: Vec<RepoInfo>, 132 - } 133 - 134 - pub async fn list_repos( 135 - State(state): State<AppState>, 136 - Query(params): Query<ListReposParams>, 137 - ) -> Response { 138 - let limit = params.limit.unwrap_or(50).min(1000); 139 - let cursor_did = params.cursor.as_deref().unwrap_or(""); 140 - 141 - let result = sqlx::query!( 142 - r#" 143 - SELECT u.did, r.repo_root_cid 144 - FROM repos r 145 - JOIN users u ON r.user_id = u.id 146 - WHERE u.did > $1 147 - ORDER BY u.did ASC 148 - LIMIT $2 149 - "#, 150 - cursor_did, 151 - limit + 1 152 - ) 153 - .fetch_all(&state.db) 154 - .await; 155 - 156 - match result { 157 - Ok(rows) => { 158 - let has_more = rows.len() as i64 > limit; 159 - let repos: Vec<RepoInfo> = rows 160 - .iter() 161 - .take(limit as usize) 162 - .map(|row| { 163 - RepoInfo { 164 - did: row.did.clone(), 165 - head: row.repo_root_cid.clone(), 166 - rev: chrono::Utc::now().timestamp_millis().to_string(), 167 - active: true, 168 - } 169 - }) 170 - .collect(); 171 - 172 - let next_cursor = if has_more { 173 - repos.last().map(|r| r.did.clone()) 174 - } else { 175 - None 176 - }; 177 - 178 - ( 179 - StatusCode::OK, 180 - Json(ListReposOutput { 181 - cursor: next_cursor, 182 - repos, 183 - }), 184 - ) 185 - .into_response() 186 - } 187 - Err(e) => { 188 - error!("DB error in list_repos: {:?}", e); 189 - ( 190 - StatusCode::INTERNAL_SERVER_ERROR, 191 - Json(json!({"error": "InternalError"})), 192 - ) 193 - .into_response() 194 - } 195 - } 196 - } 197 - 198 - #[derive(Deserialize)] 199 - pub struct GetBlobParams { 200 - pub did: String, 201 - pub cid: String, 202 - } 203 - 204 - pub async fn get_blob( 205 - State(state): State<AppState>, 206 - Query(params): Query<GetBlobParams>, 207 - ) -> Response { 208 - let did = params.did.trim(); 209 - let cid = params.cid.trim(); 210 - 211 - if did.is_empty() { 212 - return ( 213 - StatusCode::BAD_REQUEST, 214 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 215 - ) 216 - .into_response(); 217 - } 218 - 219 - if cid.is_empty() { 220 - return ( 221 - StatusCode::BAD_REQUEST, 222 - Json(json!({"error": "InvalidRequest", "message": "cid is required"})), 223 - ) 224 - .into_response(); 225 - } 226 - 227 - let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 228 - .fetch_optional(&state.db) 229 - .await; 230 - 231 - match user_exists { 232 - Ok(None) => { 233 - return ( 234 - StatusCode::NOT_FOUND, 235 - Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 236 - ) 237 - .into_response(); 238 - } 239 - Err(e) => { 240 - error!("DB error in get_blob: {:?}", e); 241 - return ( 242 - StatusCode::INTERNAL_SERVER_ERROR, 243 - Json(json!({"error": "InternalError"})), 244 - ) 245 - .into_response(); 246 - } 247 - Ok(Some(_)) => {} 248 - } 249 - 250 - let blob_result = sqlx::query!("SELECT storage_key, mime_type FROM blobs WHERE cid = $1", cid) 251 - .fetch_optional(&state.db) 252 - .await; 253 - 254 - match blob_result { 255 - Ok(Some(row)) => { 256 - let storage_key = &row.storage_key; 257 - let mime_type = &row.mime_type; 258 - 259 - match state.blob_store.get(&storage_key).await { 260 - Ok(data) => Response::builder() 261 - .status(StatusCode::OK) 262 - .header(header::CONTENT_TYPE, mime_type) 263 - .body(Body::from(data)) 264 - .unwrap(), 265 - Err(e) => { 266 - error!("Failed to fetch blob from storage: {:?}", e); 267 - ( 268 - StatusCode::NOT_FOUND, 269 - Json(json!({"error": "BlobNotFound", "message": "Blob not found in storage"})), 270 - ) 271 - .into_response() 272 - } 273 - } 274 - } 275 - Ok(None) => ( 276 - StatusCode::NOT_FOUND, 277 - Json(json!({"error": "BlobNotFound", "message": "Blob not found"})), 278 - ) 279 - .into_response(), 280 - Err(e) => { 281 - error!("DB error in get_blob: {:?}", e); 282 - ( 283 - StatusCode::INTERNAL_SERVER_ERROR, 284 - Json(json!({"error": "InternalError"})), 285 - ) 286 - .into_response() 287 - } 288 - } 289 - } 290 291 - #[derive(Deserialize)] 292 - pub struct ListBlobsParams { 293 - pub did: String, 294 - pub since: Option<String>, 295 - pub limit: Option<i64>, 296 - pub cursor: Option<String>, 297 - } 298 - 299 - #[derive(Serialize)] 300 - pub struct ListBlobsOutput { 301 - pub cursor: Option<String>, 302 - pub cids: Vec<String>, 303 - } 304 - 305 - pub async fn list_blobs( 306 - State(state): State<AppState>, 307 - Query(params): Query<ListBlobsParams>, 308 - ) -> Response { 309 - let did = params.did.trim(); 310 - 311 - if did.is_empty() { 312 - return ( 313 - StatusCode::BAD_REQUEST, 314 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 315 - ) 316 - .into_response(); 317 - } 318 - 319 - let limit = params.limit.unwrap_or(500).min(1000); 320 - let cursor_cid = params.cursor.as_deref().unwrap_or(""); 321 - 322 - let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 323 - .fetch_optional(&state.db) 324 - .await; 325 - 326 - let user_id = match user_result { 327 - Ok(Some(row)) => row.id, 328 - Ok(None) => { 329 - return ( 330 - StatusCode::NOT_FOUND, 331 - Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 332 - ) 333 - .into_response(); 334 - } 335 - Err(e) => { 336 - error!("DB error in list_blobs: {:?}", e); 337 - return ( 338 - StatusCode::INTERNAL_SERVER_ERROR, 339 - Json(json!({"error": "InternalError"})), 340 - ) 341 - .into_response(); 342 - } 343 - }; 344 - 345 - let cids_result: Result<Vec<String>, sqlx::Error> = if let Some(since) = &params.since { 346 - let since_time = chrono::DateTime::parse_from_rfc3339(since) 347 - .map(|dt| dt.with_timezone(&chrono::Utc)) 348 - .unwrap_or_else(|_| chrono::Utc::now()); 349 - sqlx::query!( 350 - r#" 351 - SELECT cid FROM blobs 352 - WHERE created_by_user = $1 AND cid > $2 AND created_at > $3 353 - ORDER BY cid ASC 354 - LIMIT $4 355 - "#, 356 - user_id, 357 - cursor_cid, 358 - since_time, 359 - limit + 1 360 - ) 361 - .fetch_all(&state.db) 362 - .await 363 - .map(|rows| rows.into_iter().map(|r| r.cid).collect()) 364 - } else { 365 - sqlx::query!( 366 - r#" 367 - SELECT cid FROM blobs 368 - WHERE created_by_user = $1 AND cid > $2 369 - ORDER BY cid ASC 370 - LIMIT $3 371 - "#, 372 - user_id, 373 - cursor_cid, 374 - limit + 1 375 - ) 376 - .fetch_all(&state.db) 377 - .await 378 - .map(|rows| rows.into_iter().map(|r| r.cid).collect()) 379 - }; 380 - 381 - match cids_result { 382 - Ok(cids) => { 383 - let has_more = cids.len() as i64 > limit; 384 - let cids: Vec<String> = cids 385 - .into_iter() 386 - .take(limit as usize) 387 - .collect(); 388 - 389 - let next_cursor = if has_more { 390 - cids.last().cloned() 391 - } else { 392 - None 393 - }; 394 - 395 - ( 396 - StatusCode::OK, 397 - Json(ListBlobsOutput { 398 - cursor: next_cursor, 399 - cids, 400 - }), 401 - ) 402 - .into_response() 403 - } 404 - Err(e) => { 405 - error!("DB error in list_blobs: {:?}", e); 406 - ( 407 - StatusCode::INTERNAL_SERVER_ERROR, 408 - Json(json!({"error": "InternalError"})), 409 - ) 410 - .into_response() 411 - } 412 - } 413 - } 414 - 415 - #[derive(Deserialize)] 416 - pub struct GetRepoStatusParams { 417 - pub did: String, 418 - } 419 - 420 - #[derive(Serialize)] 421 - pub struct GetRepoStatusOutput { 422 - pub did: String, 423 - pub active: bool, 424 - pub rev: Option<String>, 425 - } 426 - 427 - pub async fn get_repo_status( 428 - State(state): State<AppState>, 429 - Query(params): Query<GetRepoStatusParams>, 430 - ) -> Response { 431 - let did = params.did.trim(); 432 - 433 - if did.is_empty() { 434 - return ( 435 - StatusCode::BAD_REQUEST, 436 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 437 - ) 438 - .into_response(); 439 - } 440 - 441 - let result = sqlx::query!( 442 - r#" 443 - SELECT u.did, r.repo_root_cid 444 - FROM users u 445 - LEFT JOIN repos r ON u.id = r.user_id 446 - WHERE u.did = $1 447 - "#, 448 - did 449 - ) 450 - .fetch_optional(&state.db) 451 - .await; 452 - 453 - match result { 454 - Ok(Some(row)) => { 455 - let rev = Some(chrono::Utc::now().timestamp_millis().to_string()); 456 - 457 - ( 458 - StatusCode::OK, 459 - Json(GetRepoStatusOutput { 460 - did: row.did, 461 - active: true, 462 - rev, 463 - }), 464 - ) 465 - .into_response() 466 - } 467 - Ok(None) => ( 468 - StatusCode::NOT_FOUND, 469 - Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 470 - ) 471 - .into_response(), 472 - Err(e) => { 473 - error!("DB error in get_repo_status: {:?}", e); 474 - ( 475 - StatusCode::INTERNAL_SERVER_ERROR, 476 - Json(json!({"error": "InternalError"})), 477 - ) 478 - .into_response() 479 - } 480 - } 481 - } 482 - 483 - #[derive(Deserialize)] 484 - pub struct NotifyOfUpdateParams { 485 - pub hostname: String, 486 - } 487 - 488 - pub async fn notify_of_update( 489 - State(_state): State<AppState>, 490 - Query(params): Query<NotifyOfUpdateParams>, 491 - ) -> Response { 492 - info!("Received notifyOfUpdate from hostname: {}", params.hostname); 493 - // TODO: Queue job for crawler interaction or relay notification 494 - info!("TODO: Queue job for notifyOfUpdate (not implemented)"); 495 - 496 - (StatusCode::OK, Json(json!({}))).into_response() 497 - } 498 - 499 - #[derive(Deserialize)] 500 - pub struct RequestCrawlInput { 501 - pub hostname: String, 502 - } 503 - 504 - pub async fn request_crawl( 505 - State(_state): State<AppState>, 506 - Json(input): Json<RequestCrawlInput>, 507 - ) -> Response { 508 - info!("Received requestCrawl for hostname: {}", input.hostname); 509 - info!("TODO: Queue job for requestCrawl (not implemented)"); 510 - 511 - (StatusCode::OK, Json(json!({}))).into_response() 512 - } 513 - 514 - #[derive(Deserialize)] 515 - pub struct GetBlocksParams { 516 - pub did: String, 517 - pub cids: String, 518 - } 519 - 520 - pub async fn get_blocks( 521 - State(state): State<AppState>, 522 - Query(params): Query<GetBlocksParams>, 523 - ) -> Response { 524 - let did = params.did.trim(); 525 - 526 - if did.is_empty() { 527 - return ( 528 - StatusCode::BAD_REQUEST, 529 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 530 - ) 531 - .into_response(); 532 - } 533 - 534 - let cid_strings: Vec<&str> = params.cids.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); 535 - 536 - if cid_strings.is_empty() { 537 - return ( 538 - StatusCode::BAD_REQUEST, 539 - Json(json!({"error": "InvalidRequest", "message": "cids is required"})), 540 - ) 541 - .into_response(); 542 - } 543 - 544 - let repo_result = sqlx::query!( 545 - r#" 546 - SELECT r.repo_root_cid 547 - FROM repos r 548 - JOIN users u ON r.user_id = u.id 549 - WHERE u.did = $1 550 - "#, 551 - did 552 - ) 553 - .fetch_optional(&state.db) 554 - .await; 555 - 556 - let repo_root_cid_str = match repo_result { 557 - Ok(Some(row)) => row.repo_root_cid, 558 - Ok(None) => { 559 - return ( 560 - StatusCode::NOT_FOUND, 561 - Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 562 - ) 563 - .into_response(); 564 - } 565 - Err(e) => { 566 - error!("DB error in get_blocks: {:?}", e); 567 - return ( 568 - StatusCode::INTERNAL_SERVER_ERROR, 569 - Json(json!({"error": "InternalError"})), 570 - ) 571 - .into_response(); 572 - } 573 - }; 574 - 575 - let root_cid = match repo_root_cid_str.parse::<Cid>() { 576 - Ok(c) => c, 577 - Err(e) => { 578 - error!("Failed to parse root CID: {:?}", e); 579 - return ( 580 - StatusCode::INTERNAL_SERVER_ERROR, 581 - Json(json!({"error": "InternalError"})), 582 - ) 583 - .into_response(); 584 - } 585 - }; 586 - 587 - let mut requested_cids: Vec<Cid> = Vec::new(); 588 - for cid_str in &cid_strings { 589 - match cid_str.parse::<Cid>() { 590 - Ok(c) => requested_cids.push(c), 591 - Err(e) => { 592 - error!("Failed to parse CID '{}': {:?}", cid_str, e); 593 - return ( 594 - StatusCode::BAD_REQUEST, 595 - Json(json!({"error": "InvalidRequest", "message": format!("Invalid CID: {}", cid_str)})), 596 - ) 597 - .into_response(); 598 - } 599 - } 600 - } 601 - 602 - let mut buf = Vec::new(); 603 - let header = encode_car_header(&root_cid); 604 - if let Err(e) = ld_write(&mut buf, &header) { 605 - error!("Failed to write CAR header: {:?}", e); 606 - return ( 607 - StatusCode::INTERNAL_SERVER_ERROR, 608 - Json(json!({"error": "InternalError"})), 609 - ) 610 - .into_response(); 611 - } 612 - 613 - for cid in &requested_cids { 614 - let cid_bytes = cid.to_bytes(); 615 - let block_result = sqlx::query!( 616 - "SELECT data FROM blocks WHERE cid = $1", 617 - &cid_bytes 618 - ) 619 - .fetch_optional(&state.db) 620 - .await; 621 - 622 - match block_result { 623 - Ok(Some(row)) => { 624 - let mut block_data = Vec::new(); 625 - block_data.extend_from_slice(&cid_bytes); 626 - block_data.extend_from_slice(&row.data); 627 - if let Err(e) = ld_write(&mut buf, &block_data) { 628 - error!("Failed to write block: {:?}", e); 629 - return ( 630 - StatusCode::INTERNAL_SERVER_ERROR, 631 - Json(json!({"error": "InternalError"})), 632 - ) 633 - .into_response(); 634 - } 635 - } 636 - Ok(None) => { 637 - return ( 638 - StatusCode::NOT_FOUND, 639 - Json(json!({"error": "BlockNotFound", "message": format!("Block not found: {}", cid)})), 640 - ) 641 - .into_response(); 642 - } 643 - Err(e) => { 644 - error!("DB error fetching block: {:?}", e); 645 - return ( 646 - StatusCode::INTERNAL_SERVER_ERROR, 647 - Json(json!({"error": "InternalError"})), 648 - ) 649 - .into_response(); 650 - } 651 - } 652 - } 653 - 654 - Response::builder() 655 - .status(StatusCode::OK) 656 - .header(header::CONTENT_TYPE, "application/vnd.ipld.car") 657 - .body(Body::from(buf)) 658 - .unwrap() 659 - } 660 - 661 - #[derive(Deserialize)] 662 - pub struct GetRepoParams { 663 - pub did: String, 664 - pub since: Option<String>, 665 - } 666 - 667 - pub async fn get_repo( 668 - State(state): State<AppState>, 669 - Query(params): Query<GetRepoParams>, 670 - ) -> Response { 671 - let did = params.did.trim(); 672 - 673 - if did.is_empty() { 674 - return ( 675 - StatusCode::BAD_REQUEST, 676 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 677 - ) 678 - .into_response(); 679 - } 680 - 681 - let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 682 - .fetch_optional(&state.db) 683 - .await; 684 - 685 - let user_id = match user_result { 686 - Ok(Some(row)) => row.id, 687 - Ok(None) => { 688 - return ( 689 - StatusCode::NOT_FOUND, 690 - Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 691 - ) 692 - .into_response(); 693 - } 694 - Err(e) => { 695 - error!("DB error in get_repo: {:?}", e); 696 - return ( 697 - StatusCode::INTERNAL_SERVER_ERROR, 698 - Json(json!({"error": "InternalError"})), 699 - ) 700 - .into_response(); 701 - } 702 - }; 703 - 704 - let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id) 705 - .fetch_optional(&state.db) 706 - .await; 707 - 708 - let repo_root_cid_str = match repo_result { 709 - Ok(Some(row)) => row.repo_root_cid, 710 - Ok(None) => { 711 - return ( 712 - StatusCode::NOT_FOUND, 713 - Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})), 714 - ) 715 - .into_response(); 716 - } 717 - Err(e) => { 718 - error!("DB error in get_repo: {:?}", e); 719 - return ( 720 - StatusCode::INTERNAL_SERVER_ERROR, 721 - Json(json!({"error": "InternalError"})), 722 - ) 723 - .into_response(); 724 - } 725 - }; 726 - 727 - let root_cid = match repo_root_cid_str.parse::<Cid>() { 728 - Ok(c) => c, 729 - Err(e) => { 730 - error!("Failed to parse root CID: {:?}", e); 731 - return ( 732 - StatusCode::INTERNAL_SERVER_ERROR, 733 - Json(json!({"error": "InternalError"})), 734 - ) 735 - .into_response(); 736 - } 737 - }; 738 - 739 - let commit_bytes = match state.block_store.get(&root_cid).await { 740 - Ok(Some(b)) => b, 741 - Ok(None) => { 742 - error!("Commit block not found: {}", root_cid); 743 - return ( 744 - StatusCode::INTERNAL_SERVER_ERROR, 745 - Json(json!({"error": "InternalError"})), 746 - ) 747 - .into_response(); 748 - } 749 - Err(e) => { 750 - error!("Failed to load commit block: {:?}", e); 751 - return ( 752 - StatusCode::INTERNAL_SERVER_ERROR, 753 - Json(json!({"error": "InternalError"})), 754 - ) 755 - .into_response(); 756 - } 757 - }; 758 - 759 - let commit = match Commit::from_cbor(&commit_bytes) { 760 - Ok(c) => c, 761 - Err(e) => { 762 - error!("Failed to parse commit: {:?}", e); 763 - return ( 764 - StatusCode::INTERNAL_SERVER_ERROR, 765 - Json(json!({"error": "InternalError"})), 766 - ) 767 - .into_response(); 768 - } 769 - }; 770 - 771 - let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new(); 772 - let mut visited: HashSet<Vec<u8>> = HashSet::new(); 773 - 774 - collected_blocks.push((root_cid, commit_bytes.clone())); 775 - visited.insert(root_cid.to_bytes()); 776 - 777 - let mst_root_cid = commit.data; 778 - if !visited.contains(&mst_root_cid.to_bytes()) { 779 - visited.insert(mst_root_cid.to_bytes()); 780 - if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await { 781 - collected_blocks.push((mst_root_cid, data)); 782 - } 783 - } 784 - 785 - let records = sqlx::query!("SELECT record_cid FROM records WHERE repo_id = $1", user_id) 786 - .fetch_all(&state.db) 787 - .await 788 - .unwrap_or_default(); 789 - 790 - for record in records { 791 - if let Ok(cid) = record.record_cid.parse::<Cid>() { 792 - if !visited.contains(&cid.to_bytes()) { 793 - visited.insert(cid.to_bytes()); 794 - if let Ok(Some(data)) = state.block_store.get(&cid).await { 795 - collected_blocks.push((cid, data)); 796 - } 797 - } 798 - } 799 - } 800 - 801 - let mut buf = Vec::new(); 802 - let header = encode_car_header(&root_cid); 803 - if let Err(e) = ld_write(&mut buf, &header) { 804 - error!("Failed to write CAR header: {:?}", e); 805 - return ( 806 - StatusCode::INTERNAL_SERVER_ERROR, 807 - Json(json!({"error": "InternalError"})), 808 - ) 809 - .into_response(); 810 - } 811 - 812 - for (cid, data) in &collected_blocks { 813 - let mut block_data = Vec::new(); 814 - block_data.extend_from_slice(&cid.to_bytes()); 815 - block_data.extend_from_slice(data); 816 - if let Err(e) = ld_write(&mut buf, &block_data) { 817 - error!("Failed to write block: {:?}", e); 818 - return ( 819 - StatusCode::INTERNAL_SERVER_ERROR, 820 - Json(json!({"error": "InternalError"})), 821 - ) 822 - .into_response(); 823 - } 824 - } 825 - 826 - Response::builder() 827 - .status(StatusCode::OK) 828 - .header(header::CONTENT_TYPE, "application/vnd.ipld.car") 829 - .body(Body::from(buf)) 830 - .unwrap() 831 - } 832 - 833 - #[derive(Deserialize)] 834 - pub struct GetRecordParams { 835 - pub did: String, 836 - pub collection: String, 837 - pub rkey: String, 838 - } 839 - 840 - pub async fn get_record( 841 - State(state): State<AppState>, 842 - Query(params): Query<GetRecordParams>, 843 - ) -> Response { 844 - let did = params.did.trim(); 845 - let collection = params.collection.trim(); 846 - let rkey = params.rkey.trim(); 847 - 848 - if did.is_empty() { 849 - return ( 850 - StatusCode::BAD_REQUEST, 851 - Json(json!({"error": "InvalidRequest", "message": "did is required"})), 852 - ) 853 - .into_response(); 854 - } 855 - 856 - if collection.is_empty() { 857 - return ( 858 - StatusCode::BAD_REQUEST, 859 - Json(json!({"error": "InvalidRequest", "message": "collection is required"})), 860 - ) 861 - .into_response(); 862 - } 863 - 864 - if rkey.is_empty() { 865 - return ( 866 - StatusCode::BAD_REQUEST, 867 - Json(json!({"error": "InvalidRequest", "message": "rkey is required"})), 868 - ) 869 - .into_response(); 870 - } 871 - 872 - let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 873 - .fetch_optional(&state.db) 874 - .await; 875 - 876 - let user_id = match user_result { 877 - Ok(Some(row)) => row.id, 878 - Ok(None) => { 879 - return ( 880 - StatusCode::NOT_FOUND, 881 - Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 882 - ) 883 - .into_response(); 884 - } 885 - Err(e) => { 886 - error!("DB error in sync get_record: {:?}", e); 887 - return ( 888 - StatusCode::INTERNAL_SERVER_ERROR, 889 - Json(json!({"error": "InternalError"})), 890 - ) 891 - .into_response(); 892 - } 893 - }; 894 - 895 - let record_result = sqlx::query!( 896 - "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3", 897 - user_id, 898 - collection, 899 - rkey 900 - ) 901 - .fetch_optional(&state.db) 902 - .await; 903 - 904 - let record_cid_str = match record_result { 905 - Ok(Some(row)) => row.record_cid, 906 - Ok(None) => { 907 - return ( 908 - StatusCode::NOT_FOUND, 909 - Json(json!({"error": "RecordNotFound", "message": "Record not found"})), 910 - ) 911 - .into_response(); 912 - } 913 - Err(e) => { 914 - error!("DB error in sync get_record: {:?}", e); 915 - return ( 916 - StatusCode::INTERNAL_SERVER_ERROR, 917 - Json(json!({"error": "InternalError"})), 918 - ) 919 - .into_response(); 920 - } 921 - }; 922 - 923 - let record_cid = match record_cid_str.parse::<Cid>() { 924 - Ok(c) => c, 925 - Err(e) => { 926 - error!("Failed to parse record CID: {:?}", e); 927 - return ( 928 - StatusCode::INTERNAL_SERVER_ERROR, 929 - Json(json!({"error": "InternalError"})), 930 - ) 931 - .into_response(); 932 - } 933 - }; 934 - 935 - let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id) 936 - .fetch_optional(&state.db) 937 - .await; 938 - 939 - let repo_root_cid_str = match repo_result { 940 - Ok(Some(row)) => row.repo_root_cid, 941 - Ok(None) => { 942 - return ( 943 - StatusCode::NOT_FOUND, 944 - Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})), 945 - ) 946 - .into_response(); 947 - } 948 - Err(e) => { 949 - error!("DB error in sync get_record: {:?}", e); 950 - return ( 951 - StatusCode::INTERNAL_SERVER_ERROR, 952 - Json(json!({"error": "InternalError"})), 953 - ) 954 - .into_response(); 955 - } 956 - }; 957 - 958 - let root_cid = match repo_root_cid_str.parse::<Cid>() { 959 - Ok(c) => c, 960 - Err(e) => { 961 - error!("Failed to parse root CID: {:?}", e); 962 - return ( 963 - StatusCode::INTERNAL_SERVER_ERROR, 964 - Json(json!({"error": "InternalError"})), 965 - ) 966 - .into_response(); 967 - } 968 - }; 969 - 970 - let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new(); 971 - 972 - let commit_bytes = match state.block_store.get(&root_cid).await { 973 - Ok(Some(b)) => b, 974 - Ok(None) => { 975 - error!("Commit block not found: {}", root_cid); 976 - return ( 977 - StatusCode::INTERNAL_SERVER_ERROR, 978 - Json(json!({"error": "InternalError"})), 979 - ) 980 - .into_response(); 981 - } 982 - Err(e) => { 983 - error!("Failed to load commit block: {:?}", e); 984 - return ( 985 - StatusCode::INTERNAL_SERVER_ERROR, 986 - Json(json!({"error": "InternalError"})), 987 - ) 988 - .into_response(); 989 - } 990 - }; 991 - 992 - collected_blocks.push((root_cid, commit_bytes.clone())); 993 - 994 - let commit = match Commit::from_cbor(&commit_bytes) { 995 - Ok(c) => c, 996 - Err(e) => { 997 - error!("Failed to parse commit: {:?}", e); 998 - return ( 999 - StatusCode::INTERNAL_SERVER_ERROR, 1000 - Json(json!({"error": "InternalError"})), 1001 - ) 1002 - .into_response(); 1003 - } 1004 - }; 1005 - 1006 - let mst_root_cid = commit.data; 1007 - if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await { 1008 - collected_blocks.push((mst_root_cid, data)); 1009 - } 1010 - 1011 - if let Ok(Some(data)) = state.block_store.get(&record_cid).await { 1012 - collected_blocks.push((record_cid, data)); 1013 - } else { 1014 - return ( 1015 - StatusCode::NOT_FOUND, 1016 - Json(json!({"error": "RecordNotFound", "message": "Record block not found"})), 1017 - ) 1018 - .into_response(); 1019 - } 1020 - 1021 - let mut buf = Vec::new(); 1022 - let header = encode_car_header(&root_cid); 1023 - if let Err(e) = ld_write(&mut buf, &header) { 1024 - error!("Failed to write CAR header: {:?}", e); 1025 - return ( 1026 - StatusCode::INTERNAL_SERVER_ERROR, 1027 - Json(json!({"error": "InternalError"})), 1028 - ) 1029 - .into_response(); 1030 - } 1031 - 1032 - for (cid, data) in &collected_blocks { 1033 - let mut block_data = Vec::new(); 1034 - block_data.extend_from_slice(&cid.to_bytes()); 1035 - block_data.extend_from_slice(data); 1036 - if let Err(e) = ld_write(&mut buf, &block_data) { 1037 - error!("Failed to write block: {:?}", e); 1038 - return ( 1039 - StatusCode::INTERNAL_SERVER_ERROR, 1040 - Json(json!({"error": "InternalError"})), 1041 - ) 1042 - .into_response(); 1043 - } 1044 - } 1045 - 1046 - Response::builder() 1047 - .status(StatusCode::OK) 1048 - .header(header::CONTENT_TYPE, "application/vnd.ipld.car") 1049 - .body(Body::from(buf)) 1050 - .unwrap() 1051 - }
··· 1 + pub mod blob; 2 + pub mod car; 3 + pub mod commit; 4 + pub mod crawl; 5 + pub mod repo; 6 7 + pub use blob::{get_blob, list_blobs}; 8 + pub use commit::{get_latest_commit, get_repo_status, list_repos}; 9 + pub use crawl::{notify_of_update, request_crawl}; 10 + pub use repo::{get_blocks, get_record, get_repo};
+556
src/sync/repo.rs
···
··· 1 + use crate::state::AppState; 2 + use crate::sync::car::{encode_car_header, ld_write}; 3 + use axum::{ 4 + Json, 5 + body::Body, 6 + extract::{Query, State}, 7 + http::StatusCode, 8 + http::header, 9 + response::{IntoResponse, Response}, 10 + }; 11 + use bytes::Bytes; 12 + use cid::Cid; 13 + use jacquard_repo::{commit::Commit, storage::BlockStore}; 14 + use serde::Deserialize; 15 + use serde_json::json; 16 + use std::collections::HashSet; 17 + use tracing::error; 18 + 19 + #[derive(Deserialize)] 20 + pub struct GetBlocksParams { 21 + pub did: String, 22 + pub cids: String, 23 + } 24 + 25 + pub async fn get_blocks( 26 + State(state): State<AppState>, 27 + Query(params): Query<GetBlocksParams>, 28 + ) -> Response { 29 + let did = params.did.trim(); 30 + 31 + if did.is_empty() { 32 + return ( 33 + StatusCode::BAD_REQUEST, 34 + Json(json!({"error": "InvalidRequest", "message": "did is required"})), 35 + ) 36 + .into_response(); 37 + } 38 + 39 + let cid_strings: Vec<&str> = params.cids.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); 40 + 41 + if cid_strings.is_empty() { 42 + return ( 43 + StatusCode::BAD_REQUEST, 44 + Json(json!({"error": "InvalidRequest", "message": "cids is required"})), 45 + ) 46 + .into_response(); 47 + } 48 + 49 + let repo_result = sqlx::query!( 50 + r#" 51 + SELECT r.repo_root_cid 52 + FROM repos r 53 + JOIN users u ON r.user_id = u.id 54 + WHERE u.did = $1 55 + "#, 56 + did 57 + ) 58 + .fetch_optional(&state.db) 59 + .await; 60 + 61 + let repo_root_cid_str = match repo_result { 62 + Ok(Some(row)) => row.repo_root_cid, 63 + Ok(None) => { 64 + return ( 65 + StatusCode::NOT_FOUND, 66 + Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 67 + ) 68 + .into_response(); 69 + } 70 + Err(e) => { 71 + error!("DB error in get_blocks: {:?}", e); 72 + return ( 73 + StatusCode::INTERNAL_SERVER_ERROR, 74 + Json(json!({"error": "InternalError"})), 75 + ) 76 + .into_response(); 77 + } 78 + }; 79 + 80 + let root_cid = match repo_root_cid_str.parse::<Cid>() { 81 + Ok(c) => c, 82 + Err(e) => { 83 + error!("Failed to parse root CID: {:?}", e); 84 + return ( 85 + StatusCode::INTERNAL_SERVER_ERROR, 86 + Json(json!({"error": "InternalError"})), 87 + ) 88 + .into_response(); 89 + } 90 + }; 91 + 92 + let mut requested_cids: Vec<Cid> = Vec::new(); 93 + for cid_str in &cid_strings { 94 + match cid_str.parse::<Cid>() { 95 + Ok(c) => requested_cids.push(c), 96 + Err(e) => { 97 + error!("Failed to parse CID '{}': {:?}", cid_str, e); 98 + return ( 99 + StatusCode::BAD_REQUEST, 100 + Json(json!({"error": "InvalidRequest", "message": format!("Invalid CID: {}", cid_str)})), 101 + ) 102 + .into_response(); 103 + } 104 + } 105 + } 106 + 107 + let mut buf = Vec::new(); 108 + let car_header = encode_car_header(&root_cid); 109 + if let Err(e) = ld_write(&mut buf, &car_header) { 110 + error!("Failed to write CAR header: {:?}", e); 111 + return ( 112 + StatusCode::INTERNAL_SERVER_ERROR, 113 + Json(json!({"error": "InternalError"})), 114 + ) 115 + .into_response(); 116 + } 117 + 118 + for cid in &requested_cids { 119 + let cid_bytes = cid.to_bytes(); 120 + let block_result = sqlx::query!( 121 + "SELECT data FROM blocks WHERE cid = $1", 122 + &cid_bytes 123 + ) 124 + .fetch_optional(&state.db) 125 + .await; 126 + 127 + match block_result { 128 + Ok(Some(row)) => { 129 + let mut block_data = Vec::new(); 130 + block_data.extend_from_slice(&cid_bytes); 131 + block_data.extend_from_slice(&row.data); 132 + if let Err(e) = ld_write(&mut buf, &block_data) { 133 + error!("Failed to write block: {:?}", e); 134 + return ( 135 + StatusCode::INTERNAL_SERVER_ERROR, 136 + Json(json!({"error": "InternalError"})), 137 + ) 138 + .into_response(); 139 + } 140 + } 141 + Ok(None) => { 142 + return ( 143 + StatusCode::NOT_FOUND, 144 + Json(json!({"error": "BlockNotFound", "message": format!("Block not found: {}", cid)})), 145 + ) 146 + .into_response(); 147 + } 148 + Err(e) => { 149 + error!("DB error fetching block: {:?}", e); 150 + return ( 151 + StatusCode::INTERNAL_SERVER_ERROR, 152 + Json(json!({"error": "InternalError"})), 153 + ) 154 + .into_response(); 155 + } 156 + } 157 + } 158 + 159 + Response::builder() 160 + .status(StatusCode::OK) 161 + .header(header::CONTENT_TYPE, "application/vnd.ipld.car") 162 + .body(Body::from(buf)) 163 + .unwrap() 164 + } 165 + 166 + #[derive(Deserialize)] 167 + pub struct GetRepoParams { 168 + pub did: String, 169 + pub since: Option<String>, 170 + } 171 + 172 + pub async fn get_repo( 173 + State(state): State<AppState>, 174 + Query(params): Query<GetRepoParams>, 175 + ) -> Response { 176 + let did = params.did.trim(); 177 + 178 + if did.is_empty() { 179 + return ( 180 + StatusCode::BAD_REQUEST, 181 + Json(json!({"error": "InvalidRequest", "message": "did is required"})), 182 + ) 183 + .into_response(); 184 + } 185 + 186 + let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 187 + .fetch_optional(&state.db) 188 + .await; 189 + 190 + let user_id = match user_result { 191 + Ok(Some(row)) => row.id, 192 + Ok(None) => { 193 + return ( 194 + StatusCode::NOT_FOUND, 195 + Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 196 + ) 197 + .into_response(); 198 + } 199 + Err(e) => { 200 + error!("DB error in get_repo: {:?}", e); 201 + return ( 202 + StatusCode::INTERNAL_SERVER_ERROR, 203 + Json(json!({"error": "InternalError"})), 204 + ) 205 + .into_response(); 206 + } 207 + }; 208 + 209 + let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id) 210 + .fetch_optional(&state.db) 211 + .await; 212 + 213 + let repo_root_cid_str = match repo_result { 214 + Ok(Some(row)) => row.repo_root_cid, 215 + Ok(None) => { 216 + return ( 217 + StatusCode::NOT_FOUND, 218 + Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})), 219 + ) 220 + .into_response(); 221 + } 222 + Err(e) => { 223 + error!("DB error in get_repo: {:?}", e); 224 + return ( 225 + StatusCode::INTERNAL_SERVER_ERROR, 226 + Json(json!({"error": "InternalError"})), 227 + ) 228 + .into_response(); 229 + } 230 + }; 231 + 232 + let root_cid = match repo_root_cid_str.parse::<Cid>() { 233 + Ok(c) => c, 234 + Err(e) => { 235 + error!("Failed to parse root CID: {:?}", e); 236 + return ( 237 + StatusCode::INTERNAL_SERVER_ERROR, 238 + Json(json!({"error": "InternalError"})), 239 + ) 240 + .into_response(); 241 + } 242 + }; 243 + 244 + let commit_bytes = match state.block_store.get(&root_cid).await { 245 + Ok(Some(b)) => b, 246 + Ok(None) => { 247 + error!("Commit block not found: {}", root_cid); 248 + return ( 249 + StatusCode::INTERNAL_SERVER_ERROR, 250 + Json(json!({"error": "InternalError"})), 251 + ) 252 + .into_response(); 253 + } 254 + Err(e) => { 255 + error!("Failed to load commit block: {:?}", e); 256 + return ( 257 + StatusCode::INTERNAL_SERVER_ERROR, 258 + Json(json!({"error": "InternalError"})), 259 + ) 260 + .into_response(); 261 + } 262 + }; 263 + 264 + let commit = match Commit::from_cbor(&commit_bytes) { 265 + Ok(c) => c, 266 + Err(e) => { 267 + error!("Failed to parse commit: {:?}", e); 268 + return ( 269 + StatusCode::INTERNAL_SERVER_ERROR, 270 + Json(json!({"error": "InternalError"})), 271 + ) 272 + .into_response(); 273 + } 274 + }; 275 + 276 + let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new(); 277 + let mut visited: HashSet<Vec<u8>> = HashSet::new(); 278 + 279 + collected_blocks.push((root_cid, commit_bytes.clone())); 280 + visited.insert(root_cid.to_bytes()); 281 + 282 + let mst_root_cid = commit.data; 283 + if !visited.contains(&mst_root_cid.to_bytes()) { 284 + visited.insert(mst_root_cid.to_bytes()); 285 + if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await { 286 + collected_blocks.push((mst_root_cid, data)); 287 + } 288 + } 289 + 290 + let records = sqlx::query!("SELECT record_cid FROM records WHERE repo_id = $1", user_id) 291 + .fetch_all(&state.db) 292 + .await 293 + .unwrap_or_default(); 294 + 295 + for record in records { 296 + if let Ok(cid) = record.record_cid.parse::<Cid>() { 297 + if !visited.contains(&cid.to_bytes()) { 298 + visited.insert(cid.to_bytes()); 299 + if let Ok(Some(data)) = state.block_store.get(&cid).await { 300 + collected_blocks.push((cid, data)); 301 + } 302 + } 303 + } 304 + } 305 + 306 + let mut buf = Vec::new(); 307 + let car_header = encode_car_header(&root_cid); 308 + if let Err(e) = ld_write(&mut buf, &car_header) { 309 + error!("Failed to write CAR header: {:?}", e); 310 + return ( 311 + StatusCode::INTERNAL_SERVER_ERROR, 312 + Json(json!({"error": "InternalError"})), 313 + ) 314 + .into_response(); 315 + } 316 + 317 + for (cid, data) in &collected_blocks { 318 + let mut block_data = Vec::new(); 319 + block_data.extend_from_slice(&cid.to_bytes()); 320 + block_data.extend_from_slice(data); 321 + if let Err(e) = ld_write(&mut buf, &block_data) { 322 + error!("Failed to write block: {:?}", e); 323 + return ( 324 + StatusCode::INTERNAL_SERVER_ERROR, 325 + Json(json!({"error": "InternalError"})), 326 + ) 327 + .into_response(); 328 + } 329 + } 330 + 331 + Response::builder() 332 + .status(StatusCode::OK) 333 + .header(header::CONTENT_TYPE, "application/vnd.ipld.car") 334 + .body(Body::from(buf)) 335 + .unwrap() 336 + } 337 + 338 + #[derive(Deserialize)] 339 + pub struct GetRecordParams { 340 + pub did: String, 341 + pub collection: String, 342 + pub rkey: String, 343 + } 344 + 345 + pub async fn get_record( 346 + State(state): State<AppState>, 347 + Query(params): Query<GetRecordParams>, 348 + ) -> Response { 349 + let did = params.did.trim(); 350 + let collection = params.collection.trim(); 351 + let rkey = params.rkey.trim(); 352 + 353 + if did.is_empty() { 354 + return ( 355 + StatusCode::BAD_REQUEST, 356 + Json(json!({"error": "InvalidRequest", "message": "did is required"})), 357 + ) 358 + .into_response(); 359 + } 360 + 361 + if collection.is_empty() { 362 + return ( 363 + StatusCode::BAD_REQUEST, 364 + Json(json!({"error": "InvalidRequest", "message": "collection is required"})), 365 + ) 366 + .into_response(); 367 + } 368 + 369 + if rkey.is_empty() { 370 + return ( 371 + StatusCode::BAD_REQUEST, 372 + Json(json!({"error": "InvalidRequest", "message": "rkey is required"})), 373 + ) 374 + .into_response(); 375 + } 376 + 377 + let user_result = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 378 + .fetch_optional(&state.db) 379 + .await; 380 + 381 + let user_id = match user_result { 382 + Ok(Some(row)) => row.id, 383 + Ok(None) => { 384 + return ( 385 + StatusCode::NOT_FOUND, 386 + Json(json!({"error": "RepoNotFound", "message": "Could not find repo for DID"})), 387 + ) 388 + .into_response(); 389 + } 390 + Err(e) => { 391 + error!("DB error in sync get_record: {:?}", e); 392 + return ( 393 + StatusCode::INTERNAL_SERVER_ERROR, 394 + Json(json!({"error": "InternalError"})), 395 + ) 396 + .into_response(); 397 + } 398 + }; 399 + 400 + let record_result = sqlx::query!( 401 + "SELECT record_cid FROM records WHERE repo_id = $1 AND collection = $2 AND rkey = $3", 402 + user_id, 403 + collection, 404 + rkey 405 + ) 406 + .fetch_optional(&state.db) 407 + .await; 408 + 409 + let record_cid_str = match record_result { 410 + Ok(Some(row)) => row.record_cid, 411 + Ok(None) => { 412 + return ( 413 + StatusCode::NOT_FOUND, 414 + Json(json!({"error": "RecordNotFound", "message": "Record not found"})), 415 + ) 416 + .into_response(); 417 + } 418 + Err(e) => { 419 + error!("DB error in sync get_record: {:?}", e); 420 + return ( 421 + StatusCode::INTERNAL_SERVER_ERROR, 422 + Json(json!({"error": "InternalError"})), 423 + ) 424 + .into_response(); 425 + } 426 + }; 427 + 428 + let record_cid = match record_cid_str.parse::<Cid>() { 429 + Ok(c) => c, 430 + Err(e) => { 431 + error!("Failed to parse record CID: {:?}", e); 432 + return ( 433 + StatusCode::INTERNAL_SERVER_ERROR, 434 + Json(json!({"error": "InternalError"})), 435 + ) 436 + .into_response(); 437 + } 438 + }; 439 + 440 + let repo_result = sqlx::query!("SELECT repo_root_cid FROM repos WHERE user_id = $1", user_id) 441 + .fetch_optional(&state.db) 442 + .await; 443 + 444 + let repo_root_cid_str = match repo_result { 445 + Ok(Some(row)) => row.repo_root_cid, 446 + Ok(None) => { 447 + return ( 448 + StatusCode::NOT_FOUND, 449 + Json(json!({"error": "RepoNotFound", "message": "Repository not initialized"})), 450 + ) 451 + .into_response(); 452 + } 453 + Err(e) => { 454 + error!("DB error in sync get_record: {:?}", e); 455 + return ( 456 + StatusCode::INTERNAL_SERVER_ERROR, 457 + Json(json!({"error": "InternalError"})), 458 + ) 459 + .into_response(); 460 + } 461 + }; 462 + 463 + let root_cid = match repo_root_cid_str.parse::<Cid>() { 464 + Ok(c) => c, 465 + Err(e) => { 466 + error!("Failed to parse root CID: {:?}", e); 467 + return ( 468 + StatusCode::INTERNAL_SERVER_ERROR, 469 + Json(json!({"error": "InternalError"})), 470 + ) 471 + .into_response(); 472 + } 473 + }; 474 + 475 + let mut collected_blocks: Vec<(Cid, Bytes)> = Vec::new(); 476 + 477 + let commit_bytes = match state.block_store.get(&root_cid).await { 478 + Ok(Some(b)) => b, 479 + Ok(None) => { 480 + error!("Commit block not found: {}", root_cid); 481 + return ( 482 + StatusCode::INTERNAL_SERVER_ERROR, 483 + Json(json!({"error": "InternalError"})), 484 + ) 485 + .into_response(); 486 + } 487 + Err(e) => { 488 + error!("Failed to load commit block: {:?}", e); 489 + return ( 490 + StatusCode::INTERNAL_SERVER_ERROR, 491 + Json(json!({"error": "InternalError"})), 492 + ) 493 + .into_response(); 494 + } 495 + }; 496 + 497 + collected_blocks.push((root_cid, commit_bytes.clone())); 498 + 499 + let commit = match Commit::from_cbor(&commit_bytes) { 500 + Ok(c) => c, 501 + Err(e) => { 502 + error!("Failed to parse commit: {:?}", e); 503 + return ( 504 + StatusCode::INTERNAL_SERVER_ERROR, 505 + Json(json!({"error": "InternalError"})), 506 + ) 507 + .into_response(); 508 + } 509 + }; 510 + 511 + let mst_root_cid = commit.data; 512 + if let Ok(Some(data)) = state.block_store.get(&mst_root_cid).await { 513 + collected_blocks.push((mst_root_cid, data)); 514 + } 515 + 516 + if let Ok(Some(data)) = state.block_store.get(&record_cid).await { 517 + collected_blocks.push((record_cid, data)); 518 + } else { 519 + return ( 520 + StatusCode::NOT_FOUND, 521 + Json(json!({"error": "RecordNotFound", "message": "Record block not found"})), 522 + ) 523 + .into_response(); 524 + } 525 + 526 + let mut buf = Vec::new(); 527 + let car_header = encode_car_header(&root_cid); 528 + if let Err(e) = ld_write(&mut buf, &car_header) { 529 + error!("Failed to write CAR header: {:?}", e); 530 + return ( 531 + StatusCode::INTERNAL_SERVER_ERROR, 532 + Json(json!({"error": "InternalError"})), 533 + ) 534 + .into_response(); 535 + } 536 + 537 + for (cid, data) in &collected_blocks { 538 + let mut block_data = Vec::new(); 539 + block_data.extend_from_slice(&cid.to_bytes()); 540 + block_data.extend_from_slice(data); 541 + if let Err(e) = ld_write(&mut buf, &block_data) { 542 + error!("Failed to write block: {:?}", e); 543 + return ( 544 + StatusCode::INTERNAL_SERVER_ERROR, 545 + Json(json!({"error": "InternalError"})), 546 + ) 547 + .into_response(); 548 + } 549 + } 550 + 551 + Response::builder() 552 + .status(StatusCode::OK) 553 + .header(header::CONTENT_TYPE, "application/vnd.ipld.car") 554 + .body(Body::from(buf)) 555 + .unwrap() 556 + }