db: refresh_sessions migration + SQLite/Postgres adapters

This commit is contained in:
2026-06-11 14:31:46 +02:00
parent ef9ecbae06
commit 3a3f3b3889
8 changed files with 255 additions and 4 deletions

View File

@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS refresh_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
token TEXT NOT NULL UNIQUE,
expires_at TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_refresh_sessions_token ON refresh_sessions(token);
CREATE INDEX IF NOT EXISTS idx_refresh_sessions_user_id ON refresh_sessions(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_sessions_expires_at ON refresh_sessions(expires_at);

View File

@@ -22,6 +22,7 @@ mod models;
mod persons;
mod profile;
mod profile_fields;
mod refresh_sessions;
mod remote_goals;
mod user_settings;
mod users;
@@ -40,6 +41,7 @@ pub use import_profile::SqliteImportProfileRepository;
pub use import_session::SqliteImportSessionRepository;
pub use persons::{SqlitePersonAdapter, create_person_adapter};
pub use profile::SqliteMovieProfileRepository;
pub use refresh_sessions::SqliteRefreshSessionAdapter;
pub use profile_fields::SqliteProfileFieldsRepository;
pub use users::SqliteUserRepository;
pub use watch_event::{SqliteWatchEventRepository, SqliteWebhookTokenRepository};

View File

@@ -0,0 +1,113 @@
use async_trait::async_trait;
use chrono::DateTime;
use domain::{
errors::DomainError,
models::RefreshSession,
ports::RefreshSessionRepository,
value_objects::UserId,
};
use sqlx::SqlitePool;
pub struct SqliteRefreshSessionAdapter {
pool: SqlitePool,
}
impl SqliteRefreshSessionAdapter {
pub fn new(pool: SqlitePool) -> Self {
Self { pool }
}
}
fn map_err(e: sqlx::Error) -> DomainError {
DomainError::InfrastructureError(e.to_string())
}
#[async_trait]
impl RefreshSessionRepository for SqliteRefreshSessionAdapter {
async fn create(&self, session: &RefreshSession) -> Result<(), DomainError> {
sqlx::query(
"INSERT INTO refresh_sessions (id, user_id, token, expires_at, created_at)
VALUES (?, ?, ?, ?, ?)",
)
.bind(session.id.to_string())
.bind(session.user_id.value().to_string())
.bind(&session.token)
.bind(session.expires_at.to_rfc3339())
.bind(session.created_at.to_rfc3339())
.execute(&self.pool)
.await
.map_err(map_err)?;
Ok(())
}
async fn get_by_token(&self, token: &str) -> Result<Option<RefreshSession>, DomainError> {
let row = sqlx::query_as::<_, RefreshSessionRow>(
"SELECT id, user_id, token, expires_at, created_at FROM refresh_sessions WHERE token = ?",
)
.bind(token)
.fetch_optional(&self.pool)
.await
.map_err(map_err)?;
row.map(RefreshSessionRow::into_domain).transpose()
}
async fn revoke(&self, token: &str) -> Result<(), DomainError> {
sqlx::query("DELETE FROM refresh_sessions WHERE token = ?")
.bind(token)
.execute(&self.pool)
.await
.map_err(map_err)?;
Ok(())
}
async fn revoke_all_for_user(&self, user_id: &UserId) -> Result<(), DomainError> {
sqlx::query("DELETE FROM refresh_sessions WHERE user_id = ?")
.bind(user_id.value().to_string())
.execute(&self.pool)
.await
.map_err(map_err)?;
Ok(())
}
async fn delete_expired(&self) -> Result<u64, DomainError> {
let now = chrono::Utc::now().to_rfc3339();
let result = sqlx::query("DELETE FROM refresh_sessions WHERE expires_at < ?")
.bind(&now)
.execute(&self.pool)
.await
.map_err(map_err)?;
Ok(result.rows_affected())
}
}
#[derive(sqlx::FromRow)]
struct RefreshSessionRow {
id: String,
user_id: String,
token: String,
expires_at: String,
created_at: String,
}
impl RefreshSessionRow {
fn into_domain(self) -> Result<RefreshSession, DomainError> {
let id = uuid::Uuid::parse_str(&self.id)
.map_err(|e| DomainError::InfrastructureError(format!("invalid uuid: {e}")))?;
let user_id = uuid::Uuid::parse_str(&self.user_id)
.map_err(|e| DomainError::InfrastructureError(format!("invalid user_id: {e}")))?;
let expires_at = DateTime::parse_from_rfc3339(&self.expires_at)
.map_err(|e| DomainError::InfrastructureError(format!("invalid expires_at: {e}")))?
.with_timezone(&chrono::Utc);
let created_at = DateTime::parse_from_rfc3339(&self.created_at)
.map_err(|e| DomainError::InfrastructureError(format!("invalid created_at: {e}")))?
.with_timezone(&chrono::Utc);
Ok(RefreshSession {
id,
user_id: UserId::from_uuid(user_id),
token: self.token,
expires_at,
created_at,
})
}
}