Skip to main content

assay_auth/store/
mod.rs

1//! Storage traits + concrete backends for the auth crate.
2//!
3//! The traits ([`UserStore`], [`SessionStore`]) are object-safe so
4//! [`crate::ctx::AuthCtx`] can hold `Arc<dyn UserStore>`. Backend
5//! implementations live behind their respective Cargo features —
6//! `backend-postgres` and `backend-sqlite` — so a slim downstream
7//! build (`--no-default-features --features auth-jwt`) compiles
8//! without sqlx.
9//!
10//! All concrete impls assume the auth schema/attached database has
11//! already been migrated by [`crate::schema`]. Engine boot runs the
12//! migration before constructing the stores.
13
14pub mod types;
15
16#[cfg(feature = "backend-postgres")]
17pub mod postgres;
18#[cfg(feature = "backend-sqlite")]
19pub mod sqlite;
20
21pub use types::*;
22
23#[cfg(feature = "backend-postgres")]
24pub use postgres::{PostgresSessionStore, PostgresUserStore};
25#[cfg(feature = "backend-sqlite")]
26pub use sqlite::{SqliteSessionStore, SqliteUserStore};
27
28/// CRUD over `auth.users`, `auth.user_upstream`, `auth.passkeys`.
29///
30/// Methods that touch passwords go through this trait too — the
31/// password module hashes the plaintext, then asks the store to
32/// persist the resulting hash.
33#[async_trait::async_trait]
34pub trait UserStore: Send + Sync + 'static {
35    async fn create_user(&self, user: &User) -> anyhow::Result<()>;
36    async fn get_user_by_id(&self, id: &str) -> anyhow::Result<Option<User>>;
37    async fn get_user_by_email(&self, email: &str) -> anyhow::Result<Option<User>>;
38    async fn update_user(&self, user: &User) -> anyhow::Result<()>;
39
40    /// Admin: paginated user list. `limit` is clamped by the impl;
41    /// `offset` may be 0. `search` is an optional case-insensitive
42    /// substring match on `email` (or `display_name` when email is
43    /// NULL). Returns rows sorted by `created_at DESC`.
44    async fn list_users(
45        &self,
46        limit: i64,
47        offset: i64,
48        search: Option<&str>,
49    ) -> anyhow::Result<Vec<User>>;
50
51    /// Admin: total user count (after applying `search` if provided).
52    /// Used by the dashboard's pagination + the Lua wrapper.
53    async fn count_users(&self, search: Option<&str>) -> anyhow::Result<i64>;
54
55    /// Admin: hard-delete a user row + cascade dependents. Returns
56    /// `Ok(true)` iff a row was removed. The schema's
57    /// `ON DELETE CASCADE` foreign keys handle the dependents
58    /// (`auth.passkeys`, `auth.sessions`, `auth.user_upstream`).
59    async fn delete_user(&self, id: &str) -> anyhow::Result<bool>;
60
61    // Password credentials — stored as Argon2id PHC strings on `auth.users`.
62    async fn set_password_hash(&self, user_id: &str, hash: &str) -> anyhow::Result<()>;
63    async fn get_password_hash(&self, user_id: &str) -> anyhow::Result<Option<String>>;
64
65    // Passkey credentials — `auth.passkeys`.
66    async fn list_passkeys(&self, user_id: &str) -> anyhow::Result<Vec<PasskeyCred>>;
67    async fn add_passkey(&self, user_id: &str, cred: &PasskeyCred) -> anyhow::Result<()>;
68    async fn remove_passkey(&self, credential_id: &[u8]) -> anyhow::Result<bool>;
69
70    // Federated upstream links — `auth.user_upstream`.
71    async fn link_upstream(
72        &self,
73        user_id: &str,
74        provider: &str,
75        subject: &str,
76    ) -> anyhow::Result<()>;
77    async fn get_user_by_upstream(
78        &self,
79        provider: &str,
80        subject: &str,
81    ) -> anyhow::Result<Option<User>>;
82
83    /// Admin: list every (provider, subject) link for a user. Used by
84    /// the dashboard's user-detail pane to show federated identities.
85    async fn list_upstream_for_user(
86        &self,
87        user_id: &str,
88    ) -> anyhow::Result<Vec<(String, String)>>;
89}
90
91/// CRUD over `auth.sessions`. The session manager
92/// ([`crate::session::SessionManager`]) is the primary caller.
93#[async_trait::async_trait]
94pub trait SessionStore: Send + Sync + 'static {
95    async fn create(&self, session: &Session) -> anyhow::Result<()>;
96    async fn get(&self, id: &str) -> anyhow::Result<Option<Session>>;
97    async fn delete(&self, id: &str) -> anyhow::Result<bool>;
98    async fn list_for_user(&self, user_id: &str) -> anyhow::Result<Vec<Session>>;
99    async fn delete_for_user(&self, user_id: &str) -> anyhow::Result<u64>;
100    /// Drop every session whose `expires_at <= now`. Returns the row
101    /// count for visibility (logging / metrics).
102    async fn purge_expired(&self, now: f64) -> anyhow::Result<u64>;
103
104    /// Admin: paginated global session list. `user_filter` narrows to
105    /// a single user when provided. Returns rows sorted by
106    /// `created_at DESC`. Used by the dashboard's Sessions pane and
107    /// the Lua wrapper.
108    async fn list_all(
109        &self,
110        limit: i64,
111        offset: i64,
112        user_filter: Option<&str>,
113    ) -> anyhow::Result<Vec<Session>>;
114
115    /// Admin: total session count (optionally filtered by user).
116    async fn count_all(&self, user_filter: Option<&str>) -> anyhow::Result<i64>;
117}