Skip to main content

modo/auth/apikey/
sqlite.rs

1use std::future::Future;
2use std::pin::Pin;
3
4use crate::db::{ColumnMap, ConnExt, ConnQueryExt, Database, FromRow};
5use crate::error::Result;
6
7use super::backend::ApiKeyBackend;
8use super::types::ApiKeyRecord;
9
10pub(crate) struct SqliteBackend {
11    db: Database,
12}
13
14impl SqliteBackend {
15    pub fn new(db: Database) -> Self {
16        Self { db }
17    }
18}
19
20impl FromRow for ApiKeyRecord {
21    fn from_row(row: &libsql::Row) -> Result<Self> {
22        let cols = ColumnMap::from_row(row);
23        let scopes_json: String = cols.get(row, "scopes")?;
24        let scopes: Vec<String> = serde_json::from_str(&scopes_json)
25            .map_err(|e| crate::Error::internal(format!("deserialize api_keys.scopes: {e}")))?;
26
27        Ok(Self {
28            id: cols.get(row, "id")?,
29            key_hash: cols.get(row, "key_hash")?,
30            tenant_id: cols.get(row, "tenant_id")?,
31            name: cols.get(row, "name")?,
32            scopes,
33            expires_at: cols.get(row, "expires_at")?,
34            last_used_at: cols.get(row, "last_used_at")?,
35            created_at: cols.get(row, "created_at")?,
36            revoked_at: cols.get(row, "revoked_at")?,
37        })
38    }
39}
40
41impl ApiKeyBackend for SqliteBackend {
42    fn store(
43        &self,
44        record: &ApiKeyRecord,
45    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
46        let id = record.id.clone();
47        let key_hash = record.key_hash.clone();
48        let tenant_id = record.tenant_id.clone();
49        let name = record.name.clone();
50        // Vec<String> serialization to JSON is infallible
51        let scopes = serde_json::to_string(&record.scopes).unwrap();
52        let expires_at = record.expires_at.clone();
53        let created_at = record.created_at.clone();
54
55        Box::pin(async move {
56            self.db
57                .conn()
58                .execute_raw(
59                    "INSERT INTO api_keys (id, key_hash, tenant_id, name, scopes, expires_at, created_at) \
60                     VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)",
61                    libsql::params![id, key_hash, tenant_id, name, scopes, expires_at, created_at],
62                )
63                .await
64                .map_err(crate::Error::from)?;
65            Ok(())
66        })
67    }
68
69    fn lookup(
70        &self,
71        key_id: &str,
72    ) -> Pin<Box<dyn Future<Output = Result<Option<ApiKeyRecord>>> + Send + '_>> {
73        let key_id = key_id.to_owned();
74        Box::pin(async move {
75            self.db
76                .conn()
77                .query_optional::<ApiKeyRecord>(
78                    "SELECT id, key_hash, tenant_id, name, scopes, expires_at, \
79                            last_used_at, created_at, revoked_at \
80                     FROM api_keys WHERE id = ?1",
81                    libsql::params![key_id],
82                )
83                .await
84        })
85    }
86
87    fn revoke(
88        &self,
89        key_id: &str,
90        revoked_at: &str,
91    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
92        let key_id = key_id.to_owned();
93        let revoked_at = revoked_at.to_owned();
94        Box::pin(async move {
95            self.db
96                .conn()
97                .execute_raw(
98                    "UPDATE api_keys SET revoked_at = ?1 WHERE id = ?2",
99                    libsql::params![revoked_at, key_id],
100                )
101                .await
102                .map_err(crate::Error::from)?;
103            Ok(())
104        })
105    }
106
107    fn list(
108        &self,
109        tenant_id: &str,
110    ) -> Pin<Box<dyn Future<Output = Result<Vec<ApiKeyRecord>>> + Send + '_>> {
111        let tenant_id = tenant_id.to_owned();
112        Box::pin(async move {
113            self.db
114                .conn()
115                .query_all::<ApiKeyRecord>(
116                    "SELECT id, key_hash, tenant_id, name, scopes, expires_at, \
117                            last_used_at, created_at, revoked_at \
118                     FROM api_keys WHERE tenant_id = ?1 AND revoked_at IS NULL \
119                     ORDER BY created_at DESC",
120                    libsql::params![tenant_id],
121                )
122                .await
123        })
124    }
125
126    fn update_last_used(
127        &self,
128        key_id: &str,
129        timestamp: &str,
130    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
131        let key_id = key_id.to_owned();
132        let timestamp = timestamp.to_owned();
133        Box::pin(async move {
134            self.db
135                .conn()
136                .execute_raw(
137                    "UPDATE api_keys SET last_used_at = ?1 WHERE id = ?2",
138                    libsql::params![timestamp, key_id],
139                )
140                .await
141                .map_err(crate::Error::from)?;
142            Ok(())
143        })
144    }
145
146    fn update_expires_at(
147        &self,
148        key_id: &str,
149        expires_at: Option<&str>,
150    ) -> Pin<Box<dyn Future<Output = Result<()>> + Send + '_>> {
151        let key_id = key_id.to_owned();
152        let expires_at = expires_at.map(|s| s.to_owned());
153        Box::pin(async move {
154            self.db
155                .conn()
156                .execute_raw(
157                    "UPDATE api_keys SET expires_at = ?1 WHERE id = ?2",
158                    libsql::params![expires_at, key_id],
159                )
160                .await
161                .map_err(crate::Error::from)?;
162            Ok(())
163        })
164    }
165}