Skip to main content

alibi_sqlx/store/
mod.rs

1//! `SQLx`-backed persistence implementation for built-in auth tables.
2
3mod accounts;
4mod api_key_usage_phases;
5mod api_keys;
6mod bundled_schema;
7mod device_codes;
8pub mod entities;
9mod invitations;
10mod jwks;
11mod members;
12pub(crate) mod migrator;
13mod organization_roles;
14mod organizations;
15mod passkeys;
16mod sessions;
17mod teams;
18mod two_factor;
19mod users;
20mod verifications;
21mod wallets;
22
23#[doc(hidden)]
24pub mod __private_test_support {
25    pub mod bundled_schema {
26        pub use super::super::bundled_schema::BundledSchema;
27    }
28
29    pub mod migrator {
30        pub use super::super::migrator::run_migrations;
31    }
32}
33
34use crate::hooks::{DatabaseHooks, SqlxBackend, SqlxHookContext, current_request_hook_context};
35use crate::pool::{Exec, SqlxPool, SqlxTransaction};
36use crate::schema::{
37    AuthSchema, SqlxAccountModel, SqlxSessionModel, SqlxUserModel, SqlxVerificationModel,
38};
39use alibi_core::config::AuthConfig;
40use alibi_core::error::AuthResult;
41use alibi_core::store::SchemaMigrator;
42use alibi_core::store::adapter::{AfterHook, AfterHookQueue};
43use alibi_core::store::{
44    AuthTransaction, BoxedTransactionValue, TransactionStore, TransactionWork,
45};
46use async_trait::async_trait;
47use chrono::{DateTime, Utc};
48use std::marker::PhantomData;
49use std::sync::Arc;
50
51#[derive(Clone)]
52pub struct SqlxStore<S: AuthSchema> {
53    config: Arc<AuthConfig>,
54    pool: SqlxPool,
55    hooks: Vec<Arc<dyn DatabaseHooks<S, SqlxBackend>>>,
56    organization_models: crate::OrganizationModels,
57    _schema: PhantomData<S>,
58}
59
60impl<S: AuthSchema> std::fmt::Debug for SqlxStore<S> {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.debug_struct("SqlxStore").finish_non_exhaustive()
63    }
64}
65
66impl<S: AuthSchema> SqlxStore<S> {
67    #[must_use]
68    pub fn new(config: impl Into<Arc<AuthConfig>>, pool: impl Into<SqlxPool>) -> Self {
69        Self {
70            config: config.into(),
71            pool: pool.into(),
72            hooks: Vec::new(),
73            organization_models: crate::OrganizationModels::default(),
74            _schema: PhantomData,
75        }
76    }
77
78    /// Bind the organization plugin to application-owned tables.
79    #[must_use]
80    pub fn with_organization_models(mut self, models: crate::OrganizationModels) -> Self {
81        self.organization_models = models;
82        self
83    }
84
85    #[must_use]
86    pub fn with_hooks(mut self, hooks: Vec<Arc<dyn DatabaseHooks<S, SqlxBackend>>>) -> Self {
87        self.hooks = hooks;
88        self
89    }
90
91    #[must_use]
92    pub fn hook<H: DatabaseHooks<S, SqlxBackend> + 'static>(mut self, hook: H) -> Self {
93        self.hooks.push(Arc::new(hook));
94        self
95    }
96
97    #[must_use]
98    pub const fn pool(&self) -> &SqlxPool {
99        &self.pool
100    }
101
102    #[must_use]
103    pub const fn config(&self) -> &Arc<AuthConfig> {
104        &self.config
105    }
106
107    pub(crate) fn hooks(&self) -> &[Arc<dyn DatabaseHooks<S, SqlxBackend>>] {
108        &self.hooks
109    }
110
111    pub(crate) const fn exec(&self) -> Exec<'_> {
112        Exec::Pool(&self.pool)
113    }
114
115    pub(crate) fn hook_context<'a>(
116        &'a self,
117        tx: Option<&'a SqlxTransaction>,
118    ) -> SqlxHookContext<'a> {
119        SqlxHookContext {
120            config: self.config.as_ref(),
121            db: &self.pool,
122            tx,
123            request: current_request_hook_context(),
124        }
125    }
126
127    pub(crate) fn find_many_limit(&self) -> i64 {
128        // Mirrors the unsigned page binding of the reference adapter.
129        i64::try_from(self.config.advanced.database.default_find_many_limit).unwrap_or(i64::MAX)
130    }
131
132    /// Run work on a fresh transaction, committing on success. On failure the
133    /// transaction is dropped, which rolls it back, and the work's error returns.
134    pub(crate) async fn in_transaction<T>(
135        &self,
136        immediate: bool,
137        work: impl AsyncFnOnce(&SqlxTransaction) -> AuthResult<T>,
138    ) -> AuthResult<T> {
139        let transaction = self.pool.begin(immediate).await?;
140        let value = work(&transaction).await?;
141        transaction.commit().await?;
142        Ok(value)
143    }
144}
145
146/// Installs the bundled auth schema in `better_auth_migrations`, the ledger
147/// the `SeaORM` adapter also records.
148#[async_trait]
149impl<S: AuthSchema> SchemaMigrator for SqlxStore<S> {
150    async fn migrate(&self) -> AuthResult<()> {
151        migrator::run_migrations(&self.pool).await
152    }
153}
154
155struct SqlxStoreTransaction<'a, S: AuthSchema> {
156    store: &'a SqlxStore<S>,
157    tx: &'a SqlxTransaction,
158    pending_after: AfterHookQueue<S>,
159}
160
161#[async_trait]
162impl<S> AuthTransaction<S> for SqlxStoreTransaction<'_, S>
163where
164    S: AuthSchema,
165    S::User: SqlxUserModel,
166    S::Account: SqlxAccountModel,
167    S::Session: SqlxSessionModel,
168    S::Verification: SqlxVerificationModel,
169{
170    async fn list_jwks(&self) -> AuthResult<Vec<alibi_core::types::Jwk>> {
171        self.store.list_jwks_with(Exec::Tx(self.tx)).await
172    }
173    async fn get_jwk_by_id(&self, id: &str) -> AuthResult<Option<alibi_core::types::Jwk>> {
174        self.store.get_jwk_with(Exec::Tx(self.tx), id).await
175    }
176    async fn create_jwk(
177        &self,
178        data: alibi_core::types::CreateJwk,
179    ) -> AuthResult<alibi_core::types::Jwk> {
180        self.store.create_jwk_with(Exec::Tx(self.tx), data).await
181    }
182
183    async fn get_team(
184        &self,
185        organization_id: &str,
186        team_id: &str,
187    ) -> AuthResult<Option<alibi_core::types::Team>> {
188        self.store
189            .get_team_with_connection(Exec::Tx(self.tx), Some(organization_id), team_id)
190            .await
191    }
192    async fn add_team_member(
193        &self,
194        team_id: &str,
195        user_id: &str,
196        maximum: Option<f64>,
197    ) -> AuthResult<alibi_core::types::AddTeamMemberResult> {
198        self.store
199            .add_team_member_in_tx(self.tx, team_id, user_id, maximum)
200            .await
201    }
202    async fn create_member(
203        &self,
204        member: alibi_core::CreateMember,
205    ) -> AuthResult<alibi_core::types::Member> {
206        self.store
207            .create_member_with_connection(Exec::Tx(self.tx), member)
208            .await
209    }
210    async fn update_session_active_team(
211        &self,
212        token: &str,
213        team_id: Option<&str>,
214    ) -> AuthResult<S::Session> {
215        self.store
216            .update_session_scope_with_connection(
217                Exec::Tx(self.tx),
218                token,
219                sessions::SessionScope::Team(team_id),
220            )
221            .await
222    }
223    async fn update_session_active_organization(
224        &self,
225        token: &str,
226        organization_id: Option<&str>,
227    ) -> AuthResult<S::Session> {
228        self.store
229            .update_session_scope_with_connection(
230                Exec::Tx(self.tx),
231                token,
232                sessions::SessionScope::Organization(organization_id),
233            )
234            .await
235    }
236    async fn provider_verification_output(
237        &self,
238        id: &str,
239    ) -> AuthResult<Option<serde_json::Value>> {
240        users::provider_verification_output::<S::User>(Exec::Tx(self.tx), id).await
241    }
242    async fn get_user_by_id(&self, id: &str) -> AuthResult<Option<S::User>> {
243        users::find_user_by_id::<S::User>(Exec::Tx(self.tx), id, users::Lock::None).await
244    }
245    async fn create_passkey(
246        &self,
247        data: alibi_core::CreatePasskey,
248    ) -> AuthResult<alibi_core::Passkey> {
249        self.store
250            .create_passkey_with_connection(Exec::Tx(self.tx), data)
251            .await
252    }
253    async fn create_user(&self, create_user: alibi_core::CreateUser) -> AuthResult<S::User> {
254        let user = self.store.create_user_in_tx(self.tx, create_user).await?;
255        self.pending_after
256            .push(AfterHook::UserCreated(user.clone()))
257            .await;
258        Ok(user)
259    }
260
261    async fn create_user_prepared(
262        &self,
263        prepared: alibi_core::user_validation::PreparedUserCreation,
264    ) -> AuthResult<S::User> {
265        let user = self
266            .store
267            .create_user_prepared_in_tx(self.tx, prepared)
268            .await?;
269        self.pending_after
270            .push(AfterHook::UserCreated(user.clone()))
271            .await;
272        Ok(user)
273    }
274
275    async fn create_account(
276        &self,
277        create_account: alibi_core::CreateAccount,
278    ) -> AuthResult<S::Account> {
279        let account = self
280            .store
281            .create_account_in_tx(self.tx, create_account)
282            .await?;
283        self.pending_after
284            .push(AfterHook::AccountCreated(account.clone()))
285            .await;
286        Ok(account)
287    }
288
289    async fn prepare_secondary_session_update(
290        &self,
291        session: S::Session,
292        expires_at: Option<DateTime<Utc>>,
293        fields: alibi_core::field_policy::FieldValues,
294    ) -> AuthResult<Option<(S::Session, alibi_core::field_policy::FieldValues)>> {
295        self.store
296            .prepare_secondary_update_with_connection(
297                Exec::Tx(self.tx),
298                Some(self.tx),
299                session,
300                expires_at,
301                fields,
302            )
303            .await
304    }
305    async fn complete_secondary_session_update(
306        &self,
307        session: S::Session,
308        expires_at: Option<DateTime<Utc>>,
309        fields: alibi_core::field_policy::FieldValues,
310        persist: bool,
311    ) -> AuthResult<Option<S::Session>> {
312        use alibi_core::AuthSession;
313        let token = session.token().to_owned();
314        let result = self
315            .store
316            .complete_secondary_update_with_connection(
317                Exec::Tx(self.tx),
318                Some(self.tx),
319                session,
320                expires_at,
321                fields,
322                persist,
323            )
324            .await?;
325        let event = result.as_ref().map_or_else(
326            || AfterHook::SessionUpdateMissing(token),
327            |model| AfterHook::SessionUpdated(model.clone()),
328        );
329        self.pending_after.push(event).await;
330        Ok(result)
331    }
332    async fn prepare_secondary_session_creation(
333        &self,
334        input: alibi_core::CreateSession,
335        persist: bool,
336    ) -> AuthResult<S::Session> {
337        let session = self
338            .store
339            .prepare_secondary_session_in_tx(self.tx, input, persist)
340            .await?;
341        self.pending_after
342            .push(AfterHook::SessionCreated(session.clone()))
343            .await;
344        Ok(session)
345    }
346    async fn create_session(
347        &self,
348        create_session: alibi_core::CreateSession,
349    ) -> AuthResult<S::Session> {
350        let session = self
351            .store
352            .create_session_in_tx(self.tx, create_session)
353            .await?;
354        self.pending_after
355            .push(AfterHook::SessionCreated(session.clone()))
356            .await;
357        Ok(session)
358    }
359    async fn create_verification_record(
360        &self,
361        data: alibi_core::verification::VerificationCreation,
362        publication: alibi_core::verification::VerificationPublication,
363    ) -> AuthResult<Option<alibi_core::verification::VerificationSnapshot>> {
364        let snapshot = self
365            .store
366            .create_verification_record_with_connection(
367                Exec::Tx(self.tx),
368                Some(self.tx),
369                data,
370                publication,
371            )
372            .await?;
373        if let Some(snapshot) = &snapshot {
374            self.pending_after
375                .push(AfterHook::VerificationRecordCreated(snapshot.clone()))
376                .await;
377        }
378        Ok(snapshot)
379    }
380
381    async fn create_verification(
382        &self,
383        verification: alibi_core::CreateVerification,
384    ) -> AuthResult<S::Verification> {
385        let verification = self
386            .store
387            .create_verification_in_tx(self.tx, verification)
388            .await?;
389        self.pending_after
390            .push(AfterHook::VerificationCreated(verification.clone()))
391            .await;
392        Ok(verification)
393    }
394}
395
396#[async_trait]
397impl<S> TransactionStore<S> for SqlxStore<S>
398where
399    S: AuthSchema,
400    S::User: SqlxUserModel,
401    S::Account: SqlxAccountModel,
402    S::Session: SqlxSessionModel,
403    S::Verification: SqlxVerificationModel,
404{
405    async fn transaction_boxed(
406        &self,
407        work: Box<TransactionWork<S>>,
408    ) -> AuthResult<BoxedTransactionValue> {
409        let tx = self.pool.begin(false).await?;
410        let tx_store = SqlxStoreTransaction {
411            store: self,
412            tx: &tx,
413            pending_after: AfterHookQueue::new(),
414        };
415        let outcome = work(&tx_store).await;
416        let pending_after = tx_store.pending_after;
417        match outcome {
418            Ok(value) => {
419                tx.commit().await?;
420                // After hooks run only once the auth writes are durable, in
421                // operation order; rollback drops them.
422                pending_after
423                    .run(self.hooks(), &self.hook_context(None))
424                    .await?;
425                Ok(value)
426            }
427            Err(err) => {
428                tx.rollback().await?;
429                Err(err)
430            }
431        }
432    }
433}
434
435/// `FOR UPDATE` on PostgreSQL; SQLite relies on its `BEGIN IMMEDIATE` writer lock.
436pub(crate) fn lock_exclusive(sql: &mut crate::sql::Sql) {
437    if sql.engine() == crate::pool::Engine::Postgres {
438        sql.push(" FOR UPDATE");
439    }
440}
441
442/// `FOR SHARE` on PostgreSQL; SQLite relies on its `BEGIN IMMEDIATE` writer lock.
443pub(crate) fn lock_shared(sql: &mut crate::sql::Sql) {
444    if sql.engine() == crate::pool::Engine::Postgres {
445        sql.push(" FOR SHARE");
446    }
447}
448
449/// Bind raw numeric `LIMIT`/`OFFSET` values; the database validates them.
450pub(crate) fn bind_page(sql: &mut crate::sql::Sql, limit: Option<f64>, offset: Option<f64>) {
451    for (clause, number) in [(" LIMIT ", limit), (" OFFSET ", offset)] {
452        if let Some(number) = number {
453            sql.push(clause);
454            if sql.engine() == crate::pool::Engine::Postgres {
455                // node-postgres sends Number parameters as text. Binding float8
456                // instead lets PostgreSQL round fractional pages to bigint.
457                sql.bind(ryu_js::Buffer::new().format(number).to_owned());
458                sql.push("::bigint");
459            } else {
460                sql.bind(number);
461            }
462        }
463    }
464}
465
466mod oauth_token_conversion;