this repo has no description
1use crate::api::ApiError; 2use crate::auth::BearerAuth; 3use crate::state::{AppState, RateLimitKind}; 4use axum::{ 5 Json, 6 extract::State, 7 http::{HeaderMap, StatusCode}, 8 response::{IntoResponse, Response}, 9}; 10use bcrypt::verify; 11use chrono::Utc; 12use serde::{Deserialize, Serialize}; 13use serde_json::json; 14use tracing::{error, info, warn}; 15 16fn extract_client_ip(headers: &HeaderMap) -> String { 17 if let Some(forwarded) = headers.get("x-forwarded-for") 18 && let Ok(value) = forwarded.to_str() 19 && let Some(first_ip) = value.split(',').next() { 20 return first_ip.trim().to_string(); 21 } 22 if let Some(real_ip) = headers.get("x-real-ip") 23 && let Ok(value) = real_ip.to_str() { 24 return value.trim().to_string(); 25 } 26 "unknown".to_string() 27} 28 29fn normalize_handle(identifier: &str, pds_hostname: &str) -> String { 30 let suffix = format!(".{}", pds_hostname); 31 if identifier.ends_with(&suffix) { 32 identifier[..identifier.len() - suffix.len()].to_string() 33 } else { 34 identifier.to_string() 35 } 36} 37 38#[derive(Deserialize)] 39pub struct CreateSessionInput { 40 pub identifier: String, 41 pub password: String, 42} 43 44#[derive(Serialize)] 45#[serde(rename_all = "camelCase")] 46pub struct CreateSessionOutput { 47 pub access_jwt: String, 48 pub refresh_jwt: String, 49 pub handle: String, 50 pub did: String, 51} 52 53pub async fn create_session( 54 State(state): State<AppState>, 55 headers: HeaderMap, 56 Json(input): Json<CreateSessionInput>, 57) -> Response { 58 info!("create_session called"); 59 let client_ip = extract_client_ip(&headers); 60 if !state 61 .check_rate_limit(RateLimitKind::Login, &client_ip) 62 .await 63 { 64 warn!(ip = %client_ip, "Login rate limit exceeded"); 65 return ( 66 StatusCode::TOO_MANY_REQUESTS, 67 Json(json!({ 68 "error": "RateLimitExceeded", 69 "message": "Too many login attempts. Please try again later." 70 })), 71 ) 72 .into_response(); 73 } 74 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 75 let normalized_identifier = normalize_handle(&input.identifier, &pds_hostname); 76 let row = match sqlx::query!( 77 r#"SELECT 78 u.id, u.did, u.handle, u.password_hash, 79 u.email_confirmed, u.discord_verified, u.telegram_verified, u.signal_verified, 80 k.key_bytes, k.encryption_version 81 FROM users u 82 JOIN user_keys k ON u.id = k.user_id 83 WHERE u.handle = $1 OR u.email = $1"#, 84 normalized_identifier 85 ) 86 .fetch_optional(&state.db) 87 .await 88 { 89 Ok(Some(row)) => row, 90 Ok(None) => { 91 let _ = verify( 92 &input.password, 93 "$2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.VTtYw1ZzQKZqmK", 94 ); 95 warn!("User not found for login attempt"); 96 return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()) 97 .into_response(); 98 } 99 Err(e) => { 100 error!("Database error fetching user: {:?}", e); 101 return ApiError::InternalError.into_response(); 102 } 103 }; 104 let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 105 Ok(k) => k, 106 Err(e) => { 107 error!("Failed to decrypt user key: {:?}", e); 108 return ApiError::InternalError.into_response(); 109 } 110 }; 111 let password_valid = if verify(&input.password, &row.password_hash).unwrap_or(false) { 112 true 113 } else { 114 let app_passwords = sqlx::query!( 115 "SELECT password_hash FROM app_passwords WHERE user_id = $1 ORDER BY created_at DESC LIMIT 20", 116 row.id 117 ) 118 .fetch_all(&state.db) 119 .await 120 .unwrap_or_default(); 121 app_passwords 122 .iter() 123 .any(|app| verify(&input.password, &app.password_hash).unwrap_or(false)) 124 }; 125 if !password_valid { 126 warn!("Password verification failed for login attempt"); 127 return ApiError::AuthenticationFailedMsg("Invalid identifier or password".into()) 128 .into_response(); 129 } 130 let is_verified = 131 row.email_confirmed || row.discord_verified || row.telegram_verified || row.signal_verified; 132 if !is_verified { 133 warn!("Login attempt for unverified account: {}", row.did); 134 return ( 135 StatusCode::FORBIDDEN, 136 Json(json!({ 137 "error": "AccountNotVerified", 138 "message": "Please verify your account before logging in", 139 "did": row.did 140 })), 141 ) 142 .into_response(); 143 } 144 let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) { 145 Ok(m) => m, 146 Err(e) => { 147 error!("Failed to create access token: {:?}", e); 148 return ApiError::InternalError.into_response(); 149 } 150 }; 151 let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { 152 Ok(m) => m, 153 Err(e) => { 154 error!("Failed to create refresh token: {:?}", e); 155 return ApiError::InternalError.into_response(); 156 } 157 }; 158 if let Err(e) = sqlx::query!( 159 "INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)", 160 row.did, 161 access_meta.jti, 162 refresh_meta.jti, 163 access_meta.expires_at, 164 refresh_meta.expires_at 165 ) 166 .execute(&state.db) 167 .await 168 { 169 error!("Failed to insert session: {:?}", e); 170 return ApiError::InternalError.into_response(); 171 } 172 let full_handle = format!("{}.{}", row.handle, pds_hostname); 173 Json(CreateSessionOutput { 174 access_jwt: access_meta.token, 175 refresh_jwt: refresh_meta.token, 176 handle: full_handle, 177 did: row.did, 178 }) 179 .into_response() 180} 181 182pub async fn get_session( 183 State(state): State<AppState>, 184 BearerAuth(auth_user): BearerAuth, 185) -> Response { 186 match sqlx::query!( 187 r#"SELECT 188 handle, email, email_confirmed, is_admin, 189 preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel", 190 discord_verified, telegram_verified, signal_verified 191 FROM users WHERE did = $1"#, 192 auth_user.did 193 ) 194 .fetch_optional(&state.db) 195 .await 196 { 197 Ok(Some(row)) => { 198 let (preferred_channel, preferred_channel_verified) = match row.preferred_channel { 199 crate::notifications::NotificationChannel::Email => ("email", row.email_confirmed), 200 crate::notifications::NotificationChannel::Discord => ("discord", row.discord_verified), 201 crate::notifications::NotificationChannel::Telegram => ("telegram", row.telegram_verified), 202 crate::notifications::NotificationChannel::Signal => ("signal", row.signal_verified), 203 }; 204 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 205 let full_handle = format!("{}.{}", row.handle, pds_hostname); 206 Json(json!({ 207 "handle": full_handle, 208 "did": auth_user.did, 209 "email": row.email, 210 "emailConfirmed": row.email_confirmed, 211 "preferredChannel": preferred_channel, 212 "preferredChannelVerified": preferred_channel_verified, 213 "isAdmin": row.is_admin, 214 "active": true, 215 "didDoc": {} 216 })).into_response() 217 } 218 Ok(None) => ApiError::AuthenticationFailed.into_response(), 219 Err(e) => { 220 error!("Database error in get_session: {:?}", e); 221 ApiError::InternalError.into_response() 222 } 223 } 224} 225 226pub async fn delete_session( 227 State(state): State<AppState>, 228 headers: axum::http::HeaderMap, 229) -> Response { 230 let token = match crate::auth::extract_bearer_token_from_header( 231 headers.get("Authorization").and_then(|h| h.to_str().ok()), 232 ) { 233 Some(t) => t, 234 None => return ApiError::AuthenticationRequired.into_response(), 235 }; 236 let jti = match crate::auth::get_jti_from_token(&token) { 237 Ok(jti) => jti, 238 Err(_) => return ApiError::AuthenticationFailed.into_response(), 239 }; 240 let did = crate::auth::get_did_from_token(&token).ok(); 241 match sqlx::query!("DELETE FROM session_tokens WHERE access_jti = $1", jti) 242 .execute(&state.db) 243 .await 244 { 245 Ok(res) if res.rows_affected() > 0 => { 246 if let Some(did) = did { 247 let session_cache_key = format!("auth:session:{}:{}", did, jti); 248 let _ = state.cache.delete(&session_cache_key).await; 249 } 250 Json(json!({})).into_response() 251 } 252 Ok(_) => ApiError::AuthenticationFailed.into_response(), 253 Err(e) => { 254 error!("Database error in delete_session: {:?}", e); 255 ApiError::AuthenticationFailed.into_response() 256 } 257 } 258} 259 260pub async fn refresh_session( 261 State(state): State<AppState>, 262 headers: axum::http::HeaderMap, 263) -> Response { 264 let client_ip = crate::rate_limit::extract_client_ip(&headers, None); 265 if !state 266 .check_rate_limit(RateLimitKind::RefreshSession, &client_ip) 267 .await 268 { 269 tracing::warn!(ip = %client_ip, "Refresh session rate limit exceeded"); 270 return ( 271 axum::http::StatusCode::TOO_MANY_REQUESTS, 272 axum::Json(serde_json::json!({ 273 "error": "RateLimitExceeded", 274 "message": "Too many requests. Please try again later." 275 })), 276 ) 277 .into_response(); 278 } 279 let refresh_token = match crate::auth::extract_bearer_token_from_header( 280 headers.get("Authorization").and_then(|h| h.to_str().ok()), 281 ) { 282 Some(t) => t, 283 None => return ApiError::AuthenticationRequired.into_response(), 284 }; 285 let refresh_jti = match crate::auth::get_jti_from_token(&refresh_token) { 286 Ok(jti) => jti, 287 Err(_) => { 288 return ApiError::AuthenticationFailedMsg("Invalid token format".into()) 289 .into_response(); 290 } 291 }; 292 let mut tx = match state.db.begin().await { 293 Ok(tx) => tx, 294 Err(e) => { 295 error!("Failed to begin transaction: {:?}", e); 296 return ApiError::InternalError.into_response(); 297 } 298 }; 299 if let Ok(Some(session_id)) = sqlx::query_scalar!( 300 "SELECT session_id FROM used_refresh_tokens WHERE refresh_jti = $1 FOR UPDATE", 301 refresh_jti 302 ) 303 .fetch_optional(&mut *tx) 304 .await 305 { 306 warn!( 307 "Refresh token reuse detected! Revoking token family for session_id: {}", 308 session_id 309 ); 310 let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_id) 311 .execute(&mut *tx) 312 .await; 313 let _ = tx.commit().await; 314 return ApiError::ExpiredTokenMsg( 315 "Refresh token has been revoked due to suspected compromise".into(), 316 ) 317 .into_response(); 318 } 319 let session_row = match sqlx::query!( 320 r#"SELECT st.id, st.did, k.key_bytes, k.encryption_version 321 FROM session_tokens st 322 JOIN users u ON st.did = u.did 323 JOIN user_keys k ON u.id = k.user_id 324 WHERE st.refresh_jti = $1 AND st.refresh_expires_at > NOW() 325 FOR UPDATE OF st"#, 326 refresh_jti 327 ) 328 .fetch_optional(&mut *tx) 329 .await 330 { 331 Ok(Some(row)) => row, 332 Ok(None) => { 333 return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()) 334 .into_response(); 335 } 336 Err(e) => { 337 error!("Database error fetching session: {:?}", e); 338 return ApiError::InternalError.into_response(); 339 } 340 }; 341 let key_bytes = 342 match crate::config::decrypt_key(&session_row.key_bytes, session_row.encryption_version) { 343 Ok(k) => k, 344 Err(e) => { 345 error!("Failed to decrypt user key: {:?}", e); 346 return ApiError::InternalError.into_response(); 347 } 348 }; 349 if crate::auth::verify_refresh_token(&refresh_token, &key_bytes).is_err() { 350 return ApiError::AuthenticationFailedMsg("Invalid refresh token".into()).into_response(); 351 } 352 let new_access_meta = 353 match crate::auth::create_access_token_with_metadata(&session_row.did, &key_bytes) { 354 Ok(m) => m, 355 Err(e) => { 356 error!("Failed to create access token: {:?}", e); 357 return ApiError::InternalError.into_response(); 358 } 359 }; 360 let new_refresh_meta = 361 match crate::auth::create_refresh_token_with_metadata(&session_row.did, &key_bytes) { 362 Ok(m) => m, 363 Err(e) => { 364 error!("Failed to create refresh token: {:?}", e); 365 return ApiError::InternalError.into_response(); 366 } 367 }; 368 match sqlx::query!( 369 "INSERT INTO used_refresh_tokens (refresh_jti, session_id) VALUES ($1, $2) ON CONFLICT (refresh_jti) DO NOTHING", 370 refresh_jti, 371 session_row.id 372 ) 373 .execute(&mut *tx) 374 .await 375 { 376 Ok(result) if result.rows_affected() == 0 => { 377 warn!("Concurrent refresh token reuse detected for session_id: {}", session_row.id); 378 let _ = sqlx::query!("DELETE FROM session_tokens WHERE id = $1", session_row.id) 379 .execute(&mut *tx) 380 .await; 381 let _ = tx.commit().await; 382 return ApiError::ExpiredTokenMsg("Refresh token has been revoked due to suspected compromise".into()).into_response(); 383 } 384 Err(e) => { 385 error!("Failed to record used refresh token: {:?}", e); 386 return ApiError::InternalError.into_response(); 387 } 388 Ok(_) => {} 389 } 390 if let Err(e) = sqlx::query!( 391 "UPDATE session_tokens SET access_jti = $1, refresh_jti = $2, access_expires_at = $3, refresh_expires_at = $4, updated_at = NOW() WHERE id = $5", 392 new_access_meta.jti, 393 new_refresh_meta.jti, 394 new_access_meta.expires_at, 395 new_refresh_meta.expires_at, 396 session_row.id 397 ) 398 .execute(&mut *tx) 399 .await 400 { 401 error!("Database error updating session: {:?}", e); 402 return ApiError::InternalError.into_response(); 403 } 404 if let Err(e) = tx.commit().await { 405 error!("Failed to commit transaction: {:?}", e); 406 return ApiError::InternalError.into_response(); 407 } 408 match sqlx::query!( 409 r#"SELECT 410 handle, email, email_confirmed, is_admin, 411 preferred_notification_channel as "preferred_channel: crate::notifications::NotificationChannel", 412 discord_verified, telegram_verified, signal_verified 413 FROM users WHERE did = $1"#, 414 session_row.did 415 ) 416 .fetch_optional(&state.db) 417 .await 418 { 419 Ok(Some(u)) => { 420 let (preferred_channel, preferred_channel_verified) = match u.preferred_channel { 421 crate::notifications::NotificationChannel::Email => ("email", u.email_confirmed), 422 crate::notifications::NotificationChannel::Discord => ("discord", u.discord_verified), 423 crate::notifications::NotificationChannel::Telegram => ("telegram", u.telegram_verified), 424 crate::notifications::NotificationChannel::Signal => ("signal", u.signal_verified), 425 }; 426 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 427 let full_handle = format!("{}.{}", u.handle, pds_hostname); 428 Json(json!({ 429 "accessJwt": new_access_meta.token, 430 "refreshJwt": new_refresh_meta.token, 431 "handle": full_handle, 432 "did": session_row.did, 433 "email": u.email, 434 "emailConfirmed": u.email_confirmed, 435 "preferredChannel": preferred_channel, 436 "preferredChannelVerified": preferred_channel_verified, 437 "isAdmin": u.is_admin, 438 "active": true 439 })).into_response() 440 } 441 Ok(None) => { 442 error!("User not found for existing session: {}", session_row.did); 443 ApiError::InternalError.into_response() 444 } 445 Err(e) => { 446 error!("Database error fetching user: {:?}", e); 447 ApiError::InternalError.into_response() 448 } 449 } 450} 451 452#[derive(Deserialize)] 453#[serde(rename_all = "camelCase")] 454pub struct ConfirmSignupInput { 455 pub did: String, 456 pub verification_code: String, 457} 458 459#[derive(Serialize)] 460#[serde(rename_all = "camelCase")] 461pub struct ConfirmSignupOutput { 462 pub access_jwt: String, 463 pub refresh_jwt: String, 464 pub handle: String, 465 pub did: String, 466 pub email: Option<String>, 467 pub email_confirmed: bool, 468 pub preferred_channel: String, 469 pub preferred_channel_verified: bool, 470} 471 472pub async fn confirm_signup( 473 State(state): State<AppState>, 474 Json(input): Json<ConfirmSignupInput>, 475) -> Response { 476 info!("confirm_signup called for DID: {}", input.did); 477 let row = match sqlx::query!( 478 r#"SELECT 479 u.id, u.did, u.handle, u.email, 480 u.preferred_notification_channel as "channel: crate::notifications::NotificationChannel", 481 k.key_bytes, k.encryption_version 482 FROM users u 483 JOIN user_keys k ON u.id = k.user_id 484 WHERE u.did = $1"#, 485 input.did 486 ) 487 .fetch_optional(&state.db) 488 .await 489 { 490 Ok(Some(row)) => row, 491 Ok(None) => { 492 warn!("User not found for confirm_signup: {}", input.did); 493 return ApiError::InvalidRequest("Invalid DID or verification code".into()).into_response(); 494 } 495 Err(e) => { 496 error!("Database error in confirm_signup: {:?}", e); 497 return ApiError::InternalError.into_response(); 498 } 499 }; 500 501 let verification = match sqlx::query!( 502 "SELECT code, expires_at FROM channel_verifications WHERE user_id = $1 AND channel = 'email'", 503 row.id 504 ) 505 .fetch_optional(&state.db) 506 .await 507 { 508 Ok(Some(v)) => v, 509 Ok(None) => { 510 warn!("No verification code found for user: {}", input.did); 511 return ApiError::InvalidRequest("No pending verification".into()).into_response(); 512 } 513 Err(e) => { 514 error!("Database error fetching verification: {:?}", e); 515 return ApiError::InternalError.into_response(); 516 } 517 }; 518 519 if verification.code != input.verification_code { 520 warn!("Invalid verification code for user: {}", input.did); 521 return ApiError::InvalidRequest("Invalid verification code".into()).into_response(); 522 } 523 if verification.expires_at < Utc::now() { 524 warn!("Verification code expired for user: {}", input.did); 525 return ApiError::ExpiredTokenMsg("Verification code has expired".into()) 526 .into_response(); 527 } 528 529 let key_bytes = match crate::config::decrypt_key(&row.key_bytes, row.encryption_version) { 530 Ok(k) => k, 531 Err(e) => { 532 error!("Failed to decrypt user key: {:?}", e); 533 return ApiError::InternalError.into_response(); 534 } 535 }; 536 let verified_column = match row.channel { 537 crate::notifications::NotificationChannel::Email => "email_confirmed", 538 crate::notifications::NotificationChannel::Discord => "discord_verified", 539 crate::notifications::NotificationChannel::Telegram => "telegram_verified", 540 crate::notifications::NotificationChannel::Signal => "signal_verified", 541 }; 542 let update_query = format!( 543 "UPDATE users SET {} = TRUE WHERE did = $1", 544 verified_column 545 ); 546 if let Err(e) = sqlx::query(&update_query) 547 .bind(&input.did) 548 .execute(&state.db) 549 .await 550 { 551 error!("Failed to update verification status: {:?}", e); 552 return ApiError::InternalError.into_response(); 553 } 554 555 if let Err(e) = sqlx::query!( 556 "DELETE FROM channel_verifications WHERE user_id = $1 AND channel = 'email'", 557 row.id 558 ) 559 .execute(&state.db) 560 .await { 561 error!("Failed to delete verification record: {:?}", e); 562 } 563 564 let access_meta = match crate::auth::create_access_token_with_metadata(&row.did, &key_bytes) { 565 Ok(m) => m, 566 Err(e) => { 567 error!("Failed to create access token: {:?}", e); 568 return ApiError::InternalError.into_response(); 569 } 570 }; 571 let refresh_meta = match crate::auth::create_refresh_token_with_metadata(&row.did, &key_bytes) { 572 Ok(m) => m, 573 Err(e) => { 574 error!("Failed to create refresh token: {:?}", e); 575 return ApiError::InternalError.into_response(); 576 } 577 }; 578 if let Err(e) = sqlx::query!( 579 "INSERT INTO session_tokens (did, access_jti, refresh_jti, access_expires_at, refresh_expires_at) VALUES ($1, $2, $3, $4, $5)", 580 row.did, 581 access_meta.jti, 582 refresh_meta.jti, 583 access_meta.expires_at, 584 refresh_meta.expires_at 585 ) 586 .execute(&state.db) 587 .await 588 { 589 error!("Failed to insert session: {:?}", e); 590 return ApiError::InternalError.into_response(); 591 } 592 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 593 if let Err(e) = crate::notifications::enqueue_welcome(&state.db, row.id, &hostname).await { 594 warn!("Failed to enqueue welcome notification: {:?}", e); 595 } 596 let email_confirmed = matches!( 597 row.channel, 598 crate::notifications::NotificationChannel::Email 599 ); 600 let preferred_channel = match row.channel { 601 crate::notifications::NotificationChannel::Email => "email", 602 crate::notifications::NotificationChannel::Discord => "discord", 603 crate::notifications::NotificationChannel::Telegram => "telegram", 604 crate::notifications::NotificationChannel::Signal => "signal", 605 }; 606 Json(ConfirmSignupOutput { 607 access_jwt: access_meta.token, 608 refresh_jwt: refresh_meta.token, 609 handle: row.handle, 610 did: row.did, 611 email: row.email, 612 email_confirmed, 613 preferred_channel: preferred_channel.to_string(), 614 preferred_channel_verified: true, 615 }) 616 .into_response() 617} 618 619#[derive(Deserialize)] 620#[serde(rename_all = "camelCase")] 621pub struct ResendVerificationInput { 622 pub did: String, 623} 624 625pub async fn resend_verification( 626 State(state): State<AppState>, 627 Json(input): Json<ResendVerificationInput>, 628) -> Response { 629 info!("resend_verification called for DID: {}", input.did); 630 let row = match sqlx::query!( 631 r#"SELECT 632 id, handle, email, 633 preferred_notification_channel as "channel: crate::notifications::NotificationChannel", 634 discord_id, telegram_username, signal_number, 635 email_confirmed, discord_verified, telegram_verified, signal_verified 636 FROM users 637 WHERE did = $1"#, 638 input.did 639 ) 640 .fetch_optional(&state.db) 641 .await 642 { 643 Ok(Some(row)) => row, 644 Ok(None) => { 645 return ApiError::InvalidRequest("User not found".into()).into_response(); 646 } 647 Err(e) => { 648 error!("Database error in resend_verification: {:?}", e); 649 return ApiError::InternalError.into_response(); 650 } 651 }; 652 let is_verified = 653 row.email_confirmed || row.discord_verified || row.telegram_verified || row.signal_verified; 654 if is_verified { 655 return ApiError::InvalidRequest("Account is already verified".into()).into_response(); 656 } 657 let verification_code = format!("{:06}", rand::random::<u32>() % 1_000_000); 658 let code_expires_at = Utc::now() + chrono::Duration::minutes(30); 659 660 let email = row.email.clone(); 661 662 if let Err(e) = sqlx::query!( 663 r#" 664 INSERT INTO channel_verifications (user_id, channel, code, pending_identifier, expires_at) 665 VALUES ($1, 'email', $2, $3, $4) 666 ON CONFLICT (user_id, channel) DO UPDATE 667 SET code = $2, pending_identifier = $3, expires_at = $4, created_at = NOW() 668 "#, 669 row.id, 670 verification_code, 671 email, 672 code_expires_at 673 ) 674 .execute(&state.db) 675 .await 676 { 677 error!("Failed to update verification code: {:?}", e); 678 return ApiError::InternalError.into_response(); 679 } 680 let (channel_str, recipient) = match row.channel { 681 crate::notifications::NotificationChannel::Email => { 682 ("email", row.email.unwrap_or_default()) 683 } 684 crate::notifications::NotificationChannel::Discord => { 685 ("discord", row.discord_id.unwrap_or_default()) 686 } 687 crate::notifications::NotificationChannel::Telegram => { 688 ("telegram", row.telegram_username.unwrap_or_default()) 689 } 690 crate::notifications::NotificationChannel::Signal => { 691 ("signal", row.signal_number.unwrap_or_default()) 692 } 693 }; 694 if let Err(e) = crate::notifications::enqueue_signup_verification( 695 &state.db, 696 row.id, 697 channel_str, 698 &recipient, 699 &verification_code, 700 ) 701 .await 702 { 703 warn!("Failed to enqueue verification notification: {:?}", e); 704 } 705 Json(json!({"success": true})).into_response() 706} 707 708#[derive(Serialize)] 709#[serde(rename_all = "camelCase")] 710pub struct SessionInfo { 711 pub id: String, 712 pub created_at: String, 713 pub expires_at: String, 714 pub is_current: bool, 715} 716 717#[derive(Serialize)] 718#[serde(rename_all = "camelCase")] 719pub struct ListSessionsOutput { 720 pub sessions: Vec<SessionInfo>, 721} 722 723pub async fn list_sessions( 724 State(state): State<AppState>, 725 headers: HeaderMap, 726 auth: BearerAuth, 727) -> Response { 728 let current_jti = headers 729 .get("authorization") 730 .and_then(|v| v.to_str().ok()) 731 .and_then(|v| v.strip_prefix("Bearer ")) 732 .and_then(|token| crate::auth::get_jti_from_token(token).ok()); 733 let result = sqlx::query_as::<_, (i32, String, chrono::DateTime<chrono::Utc>, chrono::DateTime<chrono::Utc>)>( 734 r#" 735 SELECT id, access_jti, created_at, refresh_expires_at 736 FROM session_tokens 737 WHERE did = $1 AND refresh_expires_at > NOW() 738 ORDER BY created_at DESC 739 "#, 740 ) 741 .bind(&auth.0.did) 742 .fetch_all(&state.db) 743 .await; 744 match result { 745 Ok(rows) => { 746 let sessions: Vec<SessionInfo> = rows 747 .into_iter() 748 .map(|(id, access_jti, created_at, expires_at)| SessionInfo { 749 id: id.to_string(), 750 created_at: created_at.to_rfc3339(), 751 expires_at: expires_at.to_rfc3339(), 752 is_current: current_jti.as_ref().map_or(false, |j| j == &access_jti), 753 }) 754 .collect(); 755 (StatusCode::OK, Json(ListSessionsOutput { sessions })).into_response() 756 } 757 Err(e) => { 758 error!("DB error in list_sessions: {:?}", e); 759 ( 760 StatusCode::INTERNAL_SERVER_ERROR, 761 Json(json!({"error": "InternalError"})), 762 ) 763 .into_response() 764 } 765 } 766} 767 768#[derive(Deserialize)] 769#[serde(rename_all = "camelCase")] 770pub struct RevokeSessionInput { 771 pub session_id: String, 772} 773 774pub async fn revoke_session( 775 State(state): State<AppState>, 776 auth: BearerAuth, 777 Json(input): Json<RevokeSessionInput>, 778) -> Response { 779 let session_id: i32 = match input.session_id.parse() { 780 Ok(id) => id, 781 Err(_) => { 782 return ( 783 StatusCode::BAD_REQUEST, 784 Json(json!({"error": "InvalidRequest", "message": "Invalid session ID"})), 785 ) 786 .into_response(); 787 } 788 }; 789 let session = sqlx::query_as::<_, (String,)>( 790 "SELECT access_jti FROM session_tokens WHERE id = $1 AND did = $2", 791 ) 792 .bind(session_id) 793 .bind(&auth.0.did) 794 .fetch_optional(&state.db) 795 .await; 796 let access_jti = match session { 797 Ok(Some((jti,))) => jti, 798 Ok(None) => { 799 return ( 800 StatusCode::NOT_FOUND, 801 Json(json!({"error": "SessionNotFound", "message": "Session not found"})), 802 ) 803 .into_response(); 804 } 805 Err(e) => { 806 error!("DB error in revoke_session: {:?}", e); 807 return ( 808 StatusCode::INTERNAL_SERVER_ERROR, 809 Json(json!({"error": "InternalError"})), 810 ) 811 .into_response(); 812 } 813 }; 814 if let Err(e) = sqlx::query("DELETE FROM session_tokens WHERE id = $1") 815 .bind(session_id) 816 .execute(&state.db) 817 .await 818 { 819 error!("DB error deleting session: {:?}", e); 820 return ( 821 StatusCode::INTERNAL_SERVER_ERROR, 822 Json(json!({"error": "InternalError"})), 823 ) 824 .into_response(); 825 } 826 let cache_key = format!("auth:session:{}:{}", auth.0.did, access_jti); 827 if let Err(e) = state.cache.delete(&cache_key).await { 828 warn!("Failed to invalidate session cache: {:?}", e); 829 } 830 info!(did = %auth.0.did, session_id = %session_id, "Session revoked"); 831 (StatusCode::OK, Json(json!({}))).into_response() 832}