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