Skip to main content

codlet_sqlx/
admin.rs

1//! [`CodeAdminStore`] implementation for [`SqliteStore`].
2//!
3//! Provides metadata listing and single-record lookup for admin tooling.
4//! Never returns plaintext codes or HMAC lookup keys (RFC-030).
5
6use codlet_core::admin::{CodeAdminStore, CodeListFilter, CodeMeta};
7use codlet_core::hashing::KeyVersion;
8use codlet_core::secret::{CodeId, SubjectId};
9use codlet_core::store::error::StoreError;
10
11use crate::SqliteStore;
12
13/// Full row type returned by the admin SELECT.
14/// (id, key_version, purpose, scope, grant_payload,
15///  created_at, expires_at, used_at, used_by_subject, revoked_at)
16type AdminRow = (
17    String,         // id
18    String,         // key_version
19    Option<String>, // purpose
20    Option<String>, // scope
21    Option<String>, // grant_payload
22    i64,            // created_at
23    i64,            // expires_at
24    Option<i64>,    // used_at
25    Option<String>, // used_by_subject
26    Option<i64>,    // revoked_at
27);
28
29fn row_to_meta(row: AdminRow) -> CodeMeta {
30    let (id, kv, purpose, scope, grant, created_at, expires_at, used_at, used_by, revoked_at) = row;
31    CodeMeta {
32        id: CodeId::new(id),
33        key_version: KeyVersion::new(kv),
34        purpose,
35        scope,
36        grant,
37        created_at: Some(created_at as u64),
38        expires_at: expires_at as u64,
39        used_at: used_at.map(|t| t as u64),
40        used_by: used_by.map(SubjectId::new),
41        revoked_at: revoked_at.map(|t| t as u64),
42    }
43}
44
45impl CodeAdminStore for SqliteStore {
46    async fn list_codes(
47        &self,
48        filter: &CodeListFilter,
49        now: u64,
50    ) -> Result<Vec<CodeMeta>, StoreError> {
51        let now_i = now as i64;
52
53        let rows: Vec<AdminRow> = match (&filter.scope, filter.active_only, filter.limit) {
54            (Some(scope), true, limit) => {
55                let mut rows: Vec<AdminRow> = sqlx::query_as(
56                    "SELECT id, key_version, purpose, scope, grant_payload,
57                            created_at, expires_at, used_at, used_by_subject, revoked_at
58                     FROM codlet_codes
59                     WHERE scope = ?
60                       AND used_at    IS NULL
61                       AND revoked_at IS NULL
62                       AND expires_at  > ?
63                     ORDER BY expires_at DESC",
64                )
65                .bind(scope.as_str())
66                .bind(now_i)
67                .fetch_all(&self.pool)
68                .await
69                .map_err(|e| StoreError::Backend(e.to_string()))?;
70                if let Some(n) = limit {
71                    rows.truncate(n);
72                }
73                rows
74            }
75            (Some(scope), false, limit) => {
76                let mut rows: Vec<AdminRow> = sqlx::query_as(
77                    "SELECT id, key_version, purpose, scope, grant_payload,
78                            created_at, expires_at, used_at, used_by_subject, revoked_at
79                     FROM codlet_codes
80                     WHERE scope = ?
81                     ORDER BY expires_at DESC",
82                )
83                .bind(scope.as_str())
84                .fetch_all(&self.pool)
85                .await
86                .map_err(|e| StoreError::Backend(e.to_string()))?;
87                if let Some(n) = limit {
88                    rows.truncate(n);
89                }
90                rows
91            }
92            (None, true, limit) => {
93                let mut rows: Vec<AdminRow> = sqlx::query_as(
94                    "SELECT id, key_version, purpose, scope, grant_payload,
95                            created_at, expires_at, used_at, used_by_subject, revoked_at
96                     FROM codlet_codes
97                     WHERE used_at    IS NULL
98                       AND revoked_at IS NULL
99                       AND expires_at  > ?
100                     ORDER BY expires_at DESC",
101                )
102                .bind(now_i)
103                .fetch_all(&self.pool)
104                .await
105                .map_err(|e| StoreError::Backend(e.to_string()))?;
106                if let Some(n) = limit {
107                    rows.truncate(n);
108                }
109                rows
110            }
111            (None, false, limit) => {
112                let mut rows: Vec<AdminRow> = sqlx::query_as(
113                    "SELECT id, key_version, purpose, scope, grant_payload,
114                            created_at, expires_at, used_at, used_by_subject, revoked_at
115                     FROM codlet_codes
116                     ORDER BY expires_at DESC",
117                )
118                .fetch_all(&self.pool)
119                .await
120                .map_err(|e| StoreError::Backend(e.to_string()))?;
121                if let Some(n) = limit {
122                    rows.truncate(n);
123                }
124                rows
125            }
126        };
127
128        Ok(rows.into_iter().map(row_to_meta).collect())
129    }
130
131    async fn get_code_meta(&self, code_id: &CodeId) -> Result<Option<CodeMeta>, StoreError> {
132        let row: Option<AdminRow> = sqlx::query_as(
133            "SELECT id, key_version, purpose, scope, grant_payload,
134                    created_at, expires_at, used_at, used_by_subject, revoked_at
135             FROM codlet_codes
136             WHERE id = ?
137             LIMIT 1",
138        )
139        .bind(code_id.as_str())
140        .fetch_optional(&self.pool)
141        .await
142        .map_err(|e| StoreError::Backend(e.to_string()))?;
143
144        Ok(row.map(row_to_meta))
145    }
146}