tokens.rs (3100B)
1 // Database operations for access_tokens table 2 3 use sqlx::PgPool; 4 use anyhow::Result; 5 use uuid::Uuid; 6 use chrono::{DateTime, Utc}; 7 8 /// Access token record 9 #[derive(Debug, Clone, sqlx::FromRow)] 10 pub struct AccessToken { 11 pub id: Uuid, 12 pub session_id: Uuid, 13 pub token: String, 14 pub token_type: String, 15 pub expires_at: DateTime<Utc>, 16 pub created_at: DateTime<Utc>, 17 pub revoked: bool, 18 pub revoked_at: Option<DateTime<Utc>>, 19 } 20 21 use super::sessions::SessionStatus; 22 23 /// Data returned by get_token_with_session for backend validation 24 #[derive(Debug, Clone)] 25 pub struct TokenWithSessionData { 26 // Token fields 27 pub token_id: Uuid, 28 pub revoked: bool, 29 // Session fields 30 pub session_status: SessionStatus, 31 pub verifiable_credential: Option<serde_json::Value>, 32 } 33 34 /// Fetch token and session data for /info endpoint 35 /// 36 /// Uses UPDATE...SET revoked = revoked (no-op) for row-level locking. 37 /// Returns all data needed for backend validation. 38 /// Does NOT filter by revoked status - backend handles validation. 39 /// 40 /// Used by the /info endpoint 41 pub async fn get_token_with_session( 42 pool: &PgPool, 43 token: &str, 44 ) -> Result<Option<TokenWithSessionData>> { 45 let result = sqlx::query( 46 r#" 47 UPDATE oauth2gw.access_tokens t 48 SET revoked = t.revoked 49 FROM oauth2gw.verification_sessions s 50 WHERE t.session_id = s.id 51 AND t.token = $1 52 AND t.expires_at > NOW() 53 RETURNING 54 t.id AS token_id, 55 t.revoked, 56 s.status AS session_status, 57 s.verifiable_credential 58 "# 59 ) 60 .bind(token) 61 .fetch_optional(pool) 62 .await?; 63 64 Ok(result.map(|row: sqlx::postgres::PgRow| { 65 use sqlx::Row; 66 TokenWithSessionData { 67 token_id: row.get("token_id"), 68 revoked: row.get("revoked"), 69 session_status: row.get("session_status"), 70 verifiable_credential: row.get("verifiable_credential"), 71 } 72 })) 73 } 74 75 /// Atomically create access token and update session to completed 76 /// 77 /// This is the atomicZ operation for /token endpoint. 78 /// Both operations succeed or fail together. 79 /// 80 /// Used by the /token endpoint after validating authorization code 81 pub async fn create_token_and_complete_session( 82 pool: &PgPool, 83 session_id: Uuid, 84 token: &str, 85 token_expires_in_seconds: i64, 86 ) -> Result<AccessToken> { 87 let access_token = sqlx::query_as::<_, AccessToken>( 88 r#" 89 WITH updated_session AS ( 90 UPDATE oauth2gw.verification_sessions 91 SET status = 'completed', completed_at = NOW() 92 WHERE id = $1 93 RETURNING id 94 ) 95 INSERT INTO oauth2gw.access_tokens (session_id, token, token_type, expires_at) 96 VALUES ($1, $2, 'Bearer', NOW() + $3 * INTERVAL '1 second') 97 RETURNING id, session_id, token, token_type, expires_at, created_at, revoked, revoked_at 98 "# 99 ) 100 .bind(session_id) 101 .bind(token) 102 .bind(token_expires_in_seconds) 103 .fetch_one(pool) 104 .await?; 105 106 Ok(access_token) 107 }