1mod 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 #[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 i64::try_from(self.config.advanced.database.default_find_many_limit).unwrap_or(i64::MAX)
173 }
174
175 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#[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 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
475pub(crate) fn lock_exclusive(sql: &mut Sql) {
477 if sql.engine() == Engine::Postgres {
478 sql.push(" FOR UPDATE");
479 }
480}
481
482pub(crate) fn lock_shared(sql: &mut Sql) {
484 if sql.engine() == Engine::Postgres {
485 sql.push(" FOR SHARE");
486 }
487}
488
489pub(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 sql.bind(ryu_js::Buffer::new().format(number).to_owned());
498 sql.push("::bigint");
499 } else {
500 sql.bind(number);
501 }
502 }
503 }
504}