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}