use anyhow::Result;
use sea_orm::{ConnectionTrait, DatabaseConnection, Statement};
use crate::auth::credential::RevocationBackend;
#[derive(Debug, Clone)]
pub struct DbRevocationStore {
conn: DatabaseConnection,
}
impl DbRevocationStore {
pub fn new(conn: DatabaseConnection) -> Self {
Self { conn }
}
pub async fn ensure_schema(&self) -> Result<()> {
let backend = self.conn.get_database_backend();
self.conn
.execute_raw(Statement::from_string(
backend,
"CREATE TABLE IF NOT EXISTS jwt_revocations ( \
key TEXT PRIMARY KEY, \
not_before BIGINT NOT NULL, \
revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW() \
)",
))
.await?;
Ok(())
}
}
#[async_trait::async_trait]
impl RevocationBackend for DbRevocationStore {
async fn revoke(&self, key: &str, not_before: i64) {
let stmt = Statement::from_sql_and_values(
self.conn.get_database_backend(),
"INSERT INTO jwt_revocations (key, not_before) VALUES ($1, $2) \
ON CONFLICT (key) DO UPDATE SET not_before = GREATEST(jwt_revocations.not_before, $2)",
[key.into(), not_before.into()],
);
if let Err(e) = self.conn.execute_raw(stmt).await {
tracing::error!(key, error = %e, "DB revocation write failed");
}
}
async fn get_not_before(&self, key: &str) -> Option<i64> {
let stmt = Statement::from_sql_and_values(
self.conn.get_database_backend(),
"SELECT not_before FROM jwt_revocations WHERE key = $1",
[key.into()],
);
match self.conn.query_one_raw(stmt).await {
Ok(Some(row)) => row.try_get::<i64>("jwt_revocations", "not_before").ok(),
Ok(None) => None,
Err(e) => {
tracing::error!(key, error = %e, "DB revocation read failed; revocation check is blind for this key");
None
}
}
}
async fn cleanup_before(&self, cutoff: i64) {
let stmt = Statement::from_sql_and_values(
self.conn.get_database_backend(),
"DELETE FROM jwt_revocations WHERE not_before < $1",
[cutoff.into()],
);
if let Err(e) = self.conn.execute_raw(stmt).await {
tracing::error!(error = %e, "DB revocation cleanup failed");
}
}
}