use anyhow::Result; use sqlx::PgPool; use uuid::Uuid; use crate::error::AppError; const KEY_PREFIX: &str = "sk_"; /// Generate a new API key: `sk_<64 hex chars>` = 67 characters total. pub fn generate_api_key() -> String { use rand::RngExt; let mut bytes = [0u8; 32]; rand::rng().fill(&mut bytes); format!("{}{}", KEY_PREFIX, ::hex::encode(bytes)) } /// Return the user's existing API key, or generate and store a new one if NULL. /// Uses a transaction with atomic update to prevent TOCTOU race conditions. pub async fn ensure_api_key(pool: &PgPool, user_id: Uuid) -> Result { let mut tx = pool.begin().await?; // Lock the row and check existing key let existing: (Option,) = sqlx::query_as("SELECT api_key FROM users WHERE id = $1 FOR UPDATE") .bind(user_id) .fetch_optional(&mut *tx) .await? .ok_or(AppError::NotFoundUser)?; if let Some(key) = existing.0 { tx.commit().await?; return Ok(key); } // Generate and store new key atomically let new_key = generate_api_key(); sqlx::query("UPDATE users SET api_key = $1 WHERE id = $2") .bind(&new_key) .bind(user_id) .execute(&mut *tx) .await?; tx.commit().await?; Ok(new_key) } /// Generate a fresh API key for the user, replacing the old one. pub async fn regenerate_api_key(pool: &PgPool, user_id: Uuid) -> Result { let new_key = generate_api_key(); sqlx::query("UPDATE users SET api_key = $1 WHERE id = $2") .bind(&new_key) .bind(user_id) .execute(pool) .await?; Ok(new_key) } /// Validate a Bearer token. Returns the `user_id` if the key matches. pub async fn validate_api_key(pool: &PgPool, raw_key: &str) -> Result> { let row: Option<(Uuid,)> = sqlx::query_as("SELECT id FROM users WHERE api_key = $1") .bind(raw_key) .fetch_optional(pool) .await?; Ok(row.map(|(id,)| id)) }