//! Session persistence. `token_hash` is sha256 of the raw cookie value; //! the raw value never hits the DB. use chrono::{DateTime, Utc}; use sqlx::{PgExecutor, PgPool}; use uuid::Uuid; use crate::domain::Session; use crate::error::AppResult; /// Accepts any `PgExecutor` so callers can pass `&PgPool` for simple /// inserts or `&mut *tx` to run inside a transaction (e.g., password /// change rotates the user's sessions atomically with the hash UPDATE). pub async fn create<'e, E: PgExecutor<'e>>( executor: E, user_id: Uuid, token_hash: &[u8], expires_at: DateTime, ) -> AppResult { let row = sqlx::query_as::<_, Session>( r#" INSERT INTO sessions (user_id, token_hash, expires_at) VALUES ($1, $2, $3) RETURNING id, user_id, token_hash, created_at, expires_at "#, ) .bind(user_id) .bind(token_hash) .bind(expires_at) .fetch_one(executor) .await?; Ok(row) } pub async fn delete_all_for_user<'e, E: PgExecutor<'e>>( executor: E, user_id: Uuid, ) -> AppResult<()> { sqlx::query("DELETE FROM sessions WHERE user_id = $1") .bind(user_id) .execute(executor) .await?; Ok(()) } /// Returns the session iff `token_hash` matches and it hasn't expired. pub async fn find_active(pool: &PgPool, token_hash: &[u8]) -> AppResult> { let row = sqlx::query_as::<_, Session>( r#" SELECT id, user_id, token_hash, created_at, expires_at FROM sessions WHERE token_hash = $1 AND expires_at > now() "#, ) .bind(token_hash) .fetch_optional(pool) .await?; Ok(row) } pub async fn delete_by_token_hash(pool: &PgPool, token_hash: &[u8]) -> AppResult<()> { sqlx::query("DELETE FROM sessions WHERE token_hash = $1") .bind(token_hash) .execute(pool) .await?; Ok(()) }