//! OAuth 2.0 authorization code storage and retrieval. use chrono::{DateTime, Utc}; use sqlx::PgPool; use super::models::DbOAuthCode; use super::{SyncAppId, UserId}; use crate::error::Result; /// Store a new OAuth authorization code. #[allow(clippy::too_many_arguments)] #[tracing::instrument(skip_all)] pub async fn create_oauth_code( pool: &PgPool, code: &str, app_id: SyncAppId, user_id: UserId, code_challenge: &str, code_challenge_method: &str, redirect_uri: &str, expires_at: DateTime, ) -> Result { let row = sqlx::query_as::<_, DbOAuthCode>( r#" INSERT INTO oauth_authorization_codes (code, app_id, user_id, code_challenge, code_challenge_method, redirect_uri, expires_at) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING * "#, ) .bind(code) .bind(app_id) .bind(user_id) .bind(code_challenge) .bind(code_challenge_method) .bind(redirect_uri) .bind(expires_at) .fetch_one(pool) .await?; Ok(row) } /// Atomically consume an authorization code: mark it used and return it in one step. /// /// Returns `Some(code)` if the code was valid and successfully consumed, /// or `None` if the code was already used, expired, or does not exist. /// Because this is a single UPDATE with `used_at IS NULL` in the WHERE clause, /// concurrent requests for the same code will never both succeed. #[tracing::instrument(skip_all)] pub async fn consume_oauth_code(pool: &PgPool, code: &str) -> Result> { let row = sqlx::query_as::<_, DbOAuthCode>( r#" UPDATE oauth_authorization_codes SET used_at = NOW() WHERE code = $1 AND used_at IS NULL AND expires_at > NOW() RETURNING * "#, ) .bind(code) .fetch_optional(pool) .await?; Ok(row) } /// Delete expired or used authorization codes older than 1 hour. /// Called opportunistically from the health monitor loop. #[tracing::instrument(skip_all)] pub async fn cleanup_expired_oauth_codes(pool: &PgPool) -> Result { let result = sqlx::query( "DELETE FROM oauth_authorization_codes WHERE expires_at < NOW() - INTERVAL '1 hour' OR (used_at IS NOT NULL AND used_at < NOW() - INTERVAL '1 hour')", ) .execute(pool) .await?; Ok(result.rows_affected()) } /// Check if a redirect URI is registered for a given sync app. /// /// Returns `Ok(false)` when the app row doesn't exist or is inactive — never /// surfaces a "no rows" error to the caller. Matching is **exact-string** on /// the registered `redirect_uris` array; trailing slashes are significant /// (`https://x/cb` and `https://x/cb/` are distinct registrations), so apps /// must register every variant they intend to redirect to. #[tracing::instrument(skip_all)] pub async fn is_registered_redirect_uri( pool: &PgPool, app_id: SyncAppId, uri: &str, ) -> Result { let row: Option<(bool,)> = sqlx::query_as( "SELECT $2 = ANY(redirect_uris) FROM sync_apps WHERE id = $1 AND is_active = true", ) .bind(app_id) .bind(uri) .fetch_optional(pool) .await?; Ok(row.map(|r| r.0).unwrap_or(false)) }