torii-storage-sqlite 0.5.2

SQLite storage backend for the torii authentication ecosystem
Documentation
use async_trait::async_trait;
use sqlx::SqlitePool;
use torii_core::{
    Error, Session, UserId, error::StorageError, repositories::SessionRepository,
    session::SessionToken,
};

pub struct SqliteSessionRepository {
    pool: SqlitePool,
}

impl SqliteSessionRepository {
    pub fn new(pool: SqlitePool) -> Self {
        Self { pool }
    }
}

#[derive(Debug, Clone, sqlx::FromRow)]
struct SqliteSession {
    token: String,
    user_id: String,
    user_agent: Option<String>,
    ip_address: Option<String>,
    created_at: i64,
    updated_at: i64,
    expires_at: i64,
}

impl From<SqliteSession> for Session {
    fn from(session: SqliteSession) -> Self {
        use chrono::DateTime;
        Session {
            token: SessionToken::new(&session.token),
            user_id: UserId::new(&session.user_id),
            user_agent: session.user_agent,
            ip_address: session.ip_address,
            created_at: DateTime::from_timestamp(session.created_at, 0).expect("Invalid timestamp"),
            updated_at: DateTime::from_timestamp(session.updated_at, 0).expect("Invalid timestamp"),
            expires_at: DateTime::from_timestamp(session.expires_at, 0).expect("Invalid timestamp"),
        }
    }
}

#[async_trait]
impl SessionRepository for SqliteSessionRepository {
    async fn create(&self, session: Session) -> Result<Session, Error> {
        let token_str = match &session.token {
            SessionToken::Opaque(t) => t,
            SessionToken::Jwt(t) => t,
        };

        let sqlite_session = sqlx::query_as::<_, SqliteSession>(
            r#"
            INSERT INTO sessions (token, user_id, user_agent, ip_address, created_at, updated_at, expires_at)
            VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)
            RETURNING *
            "#,
        )
        .bind(token_str)
        .bind(session.user_id.as_str())
        .bind(&session.user_agent)
        .bind(&session.ip_address)
        .bind(session.created_at.timestamp())
        .bind(session.updated_at.timestamp())
        .bind(session.expires_at.timestamp())
        .fetch_one(&self.pool)
        .await
        .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;

        Ok(sqlite_session.into())
    }

    async fn find_by_token(&self, token: &SessionToken) -> Result<Option<Session>, Error> {
        let token_str = match token {
            SessionToken::Opaque(t) => t,
            SessionToken::Jwt(t) => t,
        };

        let sqlite_session =
            sqlx::query_as::<_, SqliteSession>("SELECT * FROM sessions WHERE token = ?1")
                .bind(token_str)
                .fetch_optional(&self.pool)
                .await
                .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;

        Ok(sqlite_session.map(|s| s.into()))
    }

    async fn delete(&self, token: &SessionToken) -> Result<(), Error> {
        let token_str = match token {
            SessionToken::Opaque(t) => t,
            SessionToken::Jwt(t) => t,
        };

        sqlx::query("DELETE FROM sessions WHERE token = ?1")
            .bind(token_str)
            .execute(&self.pool)
            .await
            .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;

        Ok(())
    }

    async fn delete_by_user_id(&self, user_id: &UserId) -> Result<(), Error> {
        sqlx::query("DELETE FROM sessions WHERE user_id = ?1")
            .bind(user_id.as_str())
            .execute(&self.pool)
            .await
            .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;

        Ok(())
    }

    async fn cleanup_expired(&self) -> Result<(), Error> {
        let now = chrono::Utc::now().timestamp();

        sqlx::query("DELETE FROM sessions WHERE expires_at < ?1")
            .bind(now)
            .execute(&self.pool)
            .await
            .map_err(|e| Error::Storage(StorageError::Database(e.to_string())))?;

        Ok(())
    }
}