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}