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