modo-rs 0.8.0

Rust web framework for small monolithic apps
Documentation
use std::future::Future;
use std::pin::Pin;

use crate::db::{ColumnMap, ConnExt, ConnQueryExt, Database, FromRow};
use crate::error::Result;

use super::backend::ApiKeyBackend;
use super::types::ApiKeyRecord;

pub(crate) struct SqliteBackend {
    db: Database,
}

impl SqliteBackend {
    pub fn new(db: Database) -> Self {
        Self { db }
    }
}

impl FromRow for ApiKeyRecord {
    fn from_row(row: &libsql::Row) -> Result<Self> {
        let cols = ColumnMap::from_row(row);
        let scopes_json: String = cols.get(row, "scopes")?;
        let scopes: Vec<String> = serde_json::from_str(&scopes_json)
            .map_err(|e| crate::Error::internal(format!("deserialize api_keys.scopes: {e}")))?;

        Ok(Self {
            id: cols.get(row, "id")?,
            key_hash: cols.get(row, "key_hash")?,
            tenant_id: cols.get(row, "tenant_id")?,
            name: cols.get(row, "name")?,
            scopes,
            expires_at: cols.get(row, "expires_at")?,
            last_used_at: cols.get(row, "last_used_at")?,
            created_at: cols.get(row, "created_at")?,
            revoked_at: cols.get(row, "revoked_at")?,
        })
    }
}

impl ApiKeyBackend for SqliteBackend {
    fn store(
        &self,
        record: &ApiKeyRecord,
    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
        let id = record.id.clone();
        let key_hash = record.key_hash.clone();
        let tenant_id = record.tenant_id.clone();
        let name = record.name.clone();
        // Vec<String> serialization to JSON is infallible
        let scopes = serde_json::to_string(&record.scopes).unwrap();
        let expires_at = record.expires_at.clone();
        let created_at = record.created_at.clone();

        Box::pin(async move {
            self.db
                .conn()
                .execute_raw(
                    "INSERT INTO api_keys (id, key_hash, tenant_id, name, scopes, expires_at, created_at) \
                     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
                    libsql::params![id, key_hash, tenant_id, name, scopes, expires_at, created_at],
                )
                .await
                .map_err(crate::Error::from)?;
            Ok(())
        })
    }

    fn lookup(
        &self,
        key_id: &str,
    ) -> Pin<Box<dyn Future<Output = Result<Option<ApiKeyRecord>>> + Send + '_>> {
        let key_id = key_id.to_owned();
        Box::pin(async move {
            self.db
                .conn()
                .query_optional::<ApiKeyRecord>(
                    "SELECT id, key_hash, tenant_id, name, scopes, expires_at, \
                            last_used_at, created_at, revoked_at \
                     FROM api_keys WHERE id = ?1",
                    libsql::params![key_id],
                )
                .await
        })
    }

    fn revoke(
        &self,
        key_id: &str,
        revoked_at: &str,
    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
        let key_id = key_id.to_owned();
        let revoked_at = revoked_at.to_owned();
        Box::pin(async move {
            self.db
                .conn()
                .execute_raw(
                    "UPDATE api_keys SET revoked_at = ?1 WHERE id = ?2",
                    libsql::params![revoked_at, key_id],
                )
                .await
                .map_err(crate::Error::from)?;
            Ok(())
        })
    }

    fn list(
        &self,
        tenant_id: &str,
    ) -> Pin<Box<dyn Future<Output = Result<Vec<ApiKeyRecord>>> + Send + '_>> {
        let tenant_id = tenant_id.to_owned();
        Box::pin(async move {
            self.db
                .conn()
                .query_all::<ApiKeyRecord>(
                    "SELECT id, key_hash, tenant_id, name, scopes, expires_at, \
                            last_used_at, created_at, revoked_at \
                     FROM api_keys WHERE tenant_id = ?1 AND revoked_at IS NULL \
                     ORDER BY created_at DESC",
                    libsql::params![tenant_id],
                )
                .await
        })
    }

    fn update_last_used(
        &self,
        key_id: &str,
        timestamp: &str,
    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
        let key_id = key_id.to_owned();
        let timestamp = timestamp.to_owned();
        Box::pin(async move {
            self.db
                .conn()
                .execute_raw(
                    "UPDATE api_keys SET last_used_at = ?1 WHERE id = ?2",
                    libsql::params![timestamp, key_id],
                )
                .await
                .map_err(crate::Error::from)?;
            Ok(())
        })
    }

    fn update_expires_at(
        &self,
        key_id: &str,
        expires_at: Option<&str>,
    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
        let key_id = key_id.to_owned();
        let expires_at = expires_at.map(|s| s.to_owned());
        Box::pin(async move {
            self.db
                .conn()
                .execute_raw(
                    "UPDATE api_keys SET expires_at = ?1 WHERE id = ?2",
                    libsql::params![expires_at, key_id],
                )
                .await
                .map_err(crate::Error::from)?;
            Ok(())
        })
    }
}