use crate::error::{AppError, Result};
use crate::storage::DbPool;
use sqlx::{Postgres, Transaction};
use time::OffsetDateTime;
use uuid::Uuid;
#[derive(Clone)]
pub struct RefreshTokenRepository {
pool: DbPool,
}
impl RefreshTokenRepository {
pub fn new(pool: DbPool) -> Self {
Self { pool }
}
pub async fn create(
&self,
tx: &mut Transaction<'_, Postgres>,
user_id: Uuid,
token_hash: &str,
ttl_days: i64,
) -> Result<()> {
let expires_at = OffsetDateTime::now_utc() + time::Duration::days(ttl_days);
sqlx::query("INSERT INTO refresh_tokens (token_hash, user_id, expires_at) VALUES ($1, $2, $3)")
.bind(token_hash)
.bind(user_id)
.bind(expires_at)
.execute(&mut **tx)
.await
.map_err(AppError::Database)?;
Ok(())
}
pub async fn verify_and_consume(
&self,
tx: &mut Transaction<'_, Postgres>,
token_hash: &str,
) -> Result<Option<Uuid>> {
#[derive(sqlx::FromRow)]
struct TokenRecord {
user_id: Uuid,
expires_at: OffsetDateTime,
}
let row: Option<TokenRecord> = sqlx::query_as(
r#"
SELECT user_id, expires_at
FROM refresh_tokens
WHERE token_hash = $1
FOR UPDATE SKIP LOCKED
"#,
)
.bind(token_hash)
.fetch_optional(&mut **tx)
.await
.map_err(AppError::Database)?;
if let Some(record) = row {
if record.expires_at < OffsetDateTime::now_utc() {
sqlx::query("DELETE FROM refresh_tokens WHERE token_hash = $1")
.bind(token_hash)
.execute(&mut **tx)
.await?;
return Ok(None);
}
sqlx::query("DELETE FROM refresh_tokens WHERE token_hash = $1").bind(token_hash).execute(&mut **tx).await?;
Ok(Some(record.user_id))
} else {
Ok(None)
}
}
pub async fn delete_owned(&self, token_hash: &str, user_id: Uuid) -> Result<()> {
sqlx::query("DELETE FROM refresh_tokens WHERE token_hash = $1 AND user_id = $2")
.bind(token_hash)
.bind(user_id)
.execute(&self.pool)
.await
.map_err(AppError::Database)?;
Ok(())
}
}