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_clients;
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, "twoFactor").await? {
205 for command in commands {
206 _ = exec.execute(Sql::with(exec.engine(), &command)).await?;
207 }
208 }
209 Ok(())
210 })
211 .await?;
212 }
213 if let Some(mapping) = &self.config.advanced.database.sso_provider {
214 let commands = mapping.migration_statements()?;
215 self.in_transaction(false, async move |tx| {
216 let exec = Exec::tx(tx);
217 if migrator::has_table(exec, "ssoProvider").await? {
218 for command in commands {
219 _ = exec.execute(Sql::with(exec.engine(), &command)).await?;
220 }
221 }
222 Ok(())
223 })
224 .await?;
225 }
226
227 let storage = &self.config.advanced.database;
228 alibi_core::store::validate_sso_fields(storage)?;
229 if !storage.sso_provider_fields.0.is_empty() {
230 let table = storage
231 .sso_provider
232 .as_ref()
233 .map_or("ssoProvider", |mapping| mapping.table_name.as_str());
234 self.in_transaction(false, async move |tx| {
235 let exec = Exec::tx(tx);
236 for (name, field) in &storage.sso_provider_fields.0 {
237 let column = alibi_core::store::sso_field_column(name, field);
238 if !migrator::has_column(exec, table, column).await? {
239 let definition = alibi_core::store::sso_field_definition(
240 name,
241 field,
242 exec.engine() == crate::pool::Engine::Postgres,
243 )?;
244 let mut ddl = Sql::with(exec.engine(), "ALTER TABLE ");
245 ddl.ident(table);
246 ddl.push(" ADD COLUMN ");
247 ddl.push(&definition);
248 _ = exec.execute(ddl).await?;
249 }
250 }
251 Ok(())
252 })
253 .await?;
254 }
255
256 Ok(())
257 }
258}
259
260struct SqlxStoreTransaction<'a, S: AuthSchema> {
261 store: &'a SqlxStore<S>,
262 tx: &'a SqlxTransaction,
263 pending_after: AfterHookQueue<S>,
264}
265
266#[async_trait]
267impl<S> AuthTransaction<S> for SqlxStoreTransaction<'_, S>
268where
269 S: AuthSchema,
270 S::User: SqlxUserModel,
271 S::Account: SqlxAccountModel,
272 S::Session: SqlxSessionModel,
273 S::Verification: SqlxVerificationModel,
274{
275 async fn get_scim_connection_binding(
276 &self,
277 id: &str,
278 ) -> AuthResult<Option<alibi_core::store::ScimConnectionBinding>> {
279 self.store
280 .get_scim_connection_binding_with_exec(Exec::tx(self.tx), id)
281 .await
282 }
283 async fn scim_projection_candidates(
284 &self,
285 domain: &str,
286 cursor: Option<&str>,
287 ) -> AuthResult<Vec<alibi_core::store::ScimProjectionCandidate>> {
288 self.store
289 .scim_projection_candidates_with_exec(Exec::tx(self.tx), domain, cursor)
290 .await
291 }
292 async fn renew_scim_decommission_lease(
293 &self,
294 binding: &alibi_core::store::ScimConnectionBinding,
295 lease: &str,
296 ) -> AuthResult<bool> {
297 self.store
298 .renew_scim_decommission_lease_with_exec(Exec::tx(self.tx), binding, lease)
299 .await
300 }
301 async fn advance_scim_decommission(
302 &self,
303 binding: &alibi_core::store::ScimConnectionBinding,
304 lease: &str,
305 cursor: Option<&str>,
306 count: usize,
307 more: bool,
308 ) -> AuthResult<Option<alibi_core::store::ScimConnectionBinding>> {
309 self.store
310 .advance_scim_decommission_with_exec(
311 Exec::tx(self.tx),
312 binding,
313 lease,
314 cursor,
315 count,
316 more,
317 )
318 .await
319 }
320 async fn reconcile_scim_identity(
321 &self,
322 user_id: &str,
323 ) -> AuthResult<alibi_core::store::ScimIdentityState> {
324 self.store
325 .reconcile_scim_identity_with_exec(Exec::tx(self.tx), user_id)
326 .await
327 }
328 async fn delete_user_sessions(&self, user_id: &str) -> AuthResult<()> {
329 use crate::model::SqlxModel;
330 let sessions = self.get_user_sessions(user_id).await?;
331 let context = self.store.hook_context(Some(self.tx));
332 for session in &sessions {
333 for hook in self.store.hooks() {
334 if hook
335 .before_delete_session(session, &context)
336 .await?
337 .is_cancelled()
338 {
339 return Ok(());
340 }
341 }
342 }
343 let exec = Exec::tx(self.tx);
344 let table = <S::Session as SqlxModel>::TABLE;
345 let id = <S::Session as SqlxSessionModel>::parse_user_id(user_id)?;
346 let mut sql = Sql::with(exec.engine(), "DELETE FROM ");
347 sql.ident(table);
348 sql.push(" WHERE ");
349 sql.compare_model::<S::Session>(table, S::Session::user_id_column(), " = ", id);
350 _ = exec.execute(sql).await?;
351 for session in sessions {
352 self.pending_after
353 .push(AfterHook::SessionDeleted(session))
354 .await;
355 }
356 Ok(())
357 }
358 async fn get_user_sessions(&self, user_id: &str) -> AuthResult<Vec<S::Session>> {
359 use crate::model::SqlxModel;
360 let exec = Exec::tx(self.tx);
361 let table = <S::Session as SqlxModel>::TABLE;
362 let mut sql = crate::model::select_model::<S::Session>(exec);
363 sql.push(" WHERE ");
364 sql.compare_model::<S::Session>(
365 table,
366 S::Session::user_id_column(),
367 " = ",
368 S::Session::parse_user_id(user_id)?,
369 );
370 sql.push(" LIMIT ");
371 sql.bind(self.store.find_many_limit());
372 exec.fetch_all::<S::Session>(sql).await
373 }
374 async fn list_oauth_tokens_for_session(
375 &self,
376 kind: alibi_core::store::OAuthTokenKind,
377 session_id: &str,
378 ) -> AuthResult<Vec<alibi_core::store::OAuthToken>> {
379 self.store
380 .session_oauth_tokens(Exec::tx(self.tx), kind, session_id)
381 .await
382 }
383 async fn get_oauth_client(
384 &self,
385 client_id: &str,
386 ) -> AuthResult<Option<alibi_core::store::OAuthClient>> {
387 self.store
388 .oauth_client_with_exec(Exec::tx(self.tx), client_id)
389 .await
390 }
391 async fn queue_session_deletion_work(
392 &self,
393 session_id: &str,
394 works: Vec<Box<dyn alibi_core::store::SessionDeletionWork<S>>>,
395 context: Arc<alibi_core::AuthContext<S>>,
396 ) -> AuthResult<()> {
397 self.pending_after
398 .attach_session_retirement(session_id, works, context)
399 .await;
400 Ok(())
401 }
402 async fn check_scim_user_attributes(
403 &self,
404 id: &str,
405 connection: &str,
406 attributes: &alibi_core::store::ScimUserAttributes,
407 ) -> AuthResult<()> {
408 self.store
409 .assert_scim_user_keys_with_exec(
410 Exec::tx(self.tx),
411 connection,
412 &attributes.user_name_key,
413 attributes.external_id_key.as_deref(),
414 None,
415 Some(id),
416 )
417 .await
418 }
419 async fn lock_scim_user(
420 &self,
421 id: &str,
422 connection: &str,
423 domain: &str,
424 attributes: Option<&alibi_core::store::ScimUserAttributes>,
425 now: chrono::DateTime<chrono::Utc>,
426 ) -> AuthResult<alibi_core::store::ScimUserMutationContext> {
427 self.store
428 .lock_scim_user_with_exec(Exec::tx(self.tx), id, connection, domain, attributes, now)
429 .await
430 }
431 async fn replace_scim_user(
432 &self,
433 id: &str,
434 data: alibi_core::store::ReplaceScimUser,
435 roles: bool,
436 ) -> AuthResult<(
437 alibi_core::store::ScimUserRecord,
438 Vec<alibi_core::store::ScimProjectionPreparedUser>,
439 )> {
440 self.store
441 .replace_scim_user_with_exec(Exec::tx(self.tx), id, data, roles)
442 .await
443 }
444 async fn create_scim_user(
445 &self,
446 data: alibi_core::store::CreateScimUser,
447 roles: bool,
448 ) -> AuthResult<(
449 alibi_core::store::ScimUserRecord,
450 Vec<alibi_core::store::ScimProjectionPreparedUser>,
451 )> {
452 self.store
453 .create_scim_user_with_exec(Exec::tx(self.tx), data, roles)
454 .await
455 }
456 async fn delete_scim_user(
457 &self,
458 id: &str,
459 connection: &str,
460 domain: &str,
461 roles: bool,
462 ) -> AuthResult<(String, Vec<alibi_core::store::ScimProjectionPreparedUser>)> {
463 self.store
464 .delete_scim_user_with_exec(Exec::tx(self.tx), id, connection, domain, roles)
465 .await
466 }
467 async fn patch_scim_group(
468 &self,
469 id: &str,
470 connection: &str,
471 domain: &str,
472 patch: alibi_core::store::ScimGroupPatch,
473 roles: bool,
474 ) -> AuthResult<(
475 alibi_core::store::ScimGroupRecord,
476 Vec<alibi_core::store::ScimProjectionPreparedUser>,
477 )> {
478 self.store
479 .patch_scim_group_with_exec(Exec::tx(self.tx), id, connection, domain, patch, roles)
480 .await
481 }
482 async fn replace_scim_group(
483 &self,
484 id: &str,
485 data: alibi_core::store::ReplaceScimGroup,
486 roles: bool,
487 ) -> AuthResult<(
488 alibi_core::store::ScimGroupRecord,
489 Vec<alibi_core::store::ScimProjectionPreparedUser>,
490 )> {
491 self.store
492 .replace_scim_group_with_exec(Exec::tx(self.tx), id, data, roles)
493 .await
494 }
495 async fn delete_scim_group(
496 &self,
497 id: &str,
498 connection: &str,
499 domain: &str,
500 roles: bool,
501 ) -> AuthResult<Vec<alibi_core::store::ScimProjectionPreparedUser>> {
502 self.store
503 .delete_scim_group_with_exec(Exec::tx(self.tx), id, connection, domain, roles)
504 .await
505 }
506 async fn create_scim_group(
507 &self,
508 data: alibi_core::store::CreateScimGroup,
509 roles: bool,
510 ) -> AuthResult<(
511 alibi_core::store::ScimGroupRecord,
512 Vec<alibi_core::store::ScimProjectionPreparedUser>,
513 )> {
514 self.store
515 .create_scim_group_with_exec(Exec::tx(self.tx), data, roles)
516 .await
517 }
518 async fn fence_scim_connection(&self, id: &str, key: &str) -> AuthResult<()> {
519 self.store
520 .fence_scim_connection_with_exec(Exec::tx(self.tx), id, key)
521 .await
522 }
523 async fn prepare_scim_projection(
524 &self,
525 domain: &str,
526 ids: &[String],
527 roles: bool,
528 ) -> AuthResult<Vec<alibi_core::store::ScimProjectionPreparedUser>> {
529 self.store
530 .prepare_scim_projection_with_exec(Exec::tx(self.tx), domain, ids, roles)
531 .await
532 }
533 async fn create_scim_projection_grant(
534 &self,
535 data: alibi_core::store::CreateScimProjectionGrant,
536 ) -> AuthResult<()> {
537 self.store
538 .create_scim_projection_grant_with_exec(Exec::tx(self.tx), data)
539 .await
540 }
541 async fn remove_scim_projection_grants(
542 &self,
543 domain: &str,
544 user_id: &str,
545 ids: &[String],
546 ) -> AuthResult<()> {
547 self.store
548 .remove_scim_projection_grants_with_exec(Exec::tx(self.tx), domain, user_id, ids)
549 .await
550 }
551 async fn get_user_by_email(&self, email: &str) -> AuthResult<Option<S::User>> {
552 self.store
553 .get_user_by_email_with_connection(Exec::tx(self.tx), email)
554 .await
555 }
556 async fn get_account(
557 &self,
558 provider: &str,
559 account_id: &str,
560 ) -> AuthResult<Option<S::Account>> {
561 self.store
562 .get_account_with_connection(Exec::tx(self.tx), provider, account_id)
563 .await
564 }
565 async fn update_user(&self, id: &str, update: alibi_core::UpdateUser) -> AuthResult<S::User> {
566 let user = self
567 .store
568 .update_user_with_connection(Exec::tx(self.tx), Some(self.tx), id, update)
569 .await?;
570 self.pending_after
571 .push(AfterHook::UserUpdated(user.clone()))
572 .await;
573 Ok(user)
574 }
575 async fn update_account(
576 &self,
577 id: &str,
578 update: alibi_core::UpdateAccount,
579 ) -> AuthResult<S::Account> {
580 let account = self
581 .store
582 .update_account_with_connection(Exec::tx(self.tx), Some(self.tx), id, update)
583 .await?;
584 self.pending_after
585 .push(AfterHook::AccountUpdated(account.clone()))
586 .await;
587 Ok(account)
588 }
589 async fn lock_sso_provider(
590 &self,
591 id: &str,
592 provider_id: &str,
593 ) -> AuthResult<Option<alibi_core::store::SsoProvider>> {
594 self.store
595 .lock_sso_provider_with(Exec::tx(self.tx), id, provider_id)
596 .await
597 }
598 async fn update_sso_provider(
599 &self,
600 update: alibi_core::store::SsoProviderUpdate,
601 ) -> AuthResult<Option<alibi_core::store::SsoProvider>> {
602 self.store
603 .update_sso_provider_with(Exec::tx(self.tx), update)
604 .await
605 }
606 async fn delete_sso_provider_and_accounts(
607 &self,
608 id: &str,
609 provider_id: &str,
610 ) -> AuthResult<bool> {
611 self.store
612 .delete_sso_provider_with(Exec::tx(self.tx), id, provider_id)
613 .await
614 }
615 async fn list_jwks(&self) -> AuthResult<Vec<Jwk>> {
616 self.store.list_jwks_with(Exec::tx(self.tx)).await
617 }
618 async fn get_jwk_by_id(&self, id: &str) -> AuthResult<Option<Jwk>> {
619 self.store.get_jwk_with(Exec::tx(self.tx), id).await
620 }
621 async fn create_jwk(&self, data: CreateJwk) -> AuthResult<Jwk> {
622 self.store.create_jwk_with(Exec::tx(self.tx), data).await
623 }
624
625 async fn get_team(&self, organization_id: &str, team_id: &str) -> AuthResult<Option<Team>> {
626 self.store
627 .get_team_with_connection(Exec::tx(self.tx), Some(organization_id), team_id)
628 .await
629 }
630 async fn add_team_member(
631 &self,
632 team_id: &str,
633 user_id: &str,
634 maximum: Option<f64>,
635 ) -> AuthResult<AddTeamMemberResult> {
636 self.store
637 .add_team_member_in_tx(self.tx, team_id, user_id, maximum)
638 .await
639 }
640 async fn create_member(&self, member: CreateMember) -> AuthResult<Member> {
641 self.store
642 .create_member_with_connection(Exec::tx(self.tx), member)
643 .await
644 }
645 async fn update_session_active_team(
646 &self,
647 token: &str,
648 team_id: Option<&str>,
649 ) -> AuthResult<S::Session> {
650 self.store
651 .update_session_scope_with_connection(
652 Exec::tx(self.tx),
653 token,
654 sessions::SessionScope::Team(team_id),
655 )
656 .await
657 }
658 async fn update_session_active_organization(
659 &self,
660 token: &str,
661 organization_id: Option<&str>,
662 ) -> AuthResult<S::Session> {
663 self.store
664 .update_session_scope_with_connection(
665 Exec::tx(self.tx),
666 token,
667 sessions::SessionScope::Organization(organization_id),
668 )
669 .await
670 }
671 async fn provider_verification_output(
672 &self,
673 id: &str,
674 ) -> AuthResult<Option<serde_json::Value>> {
675 users::provider_verification_output::<S::User>(Exec::tx(self.tx), id).await
676 }
677 async fn get_user_by_id(&self, id: &str) -> AuthResult<Option<S::User>> {
678 users::find_user_by_id::<S::User>(Exec::tx(self.tx), id, users::Lock::None).await
679 }
680 async fn create_passkey(&self, data: CreatePasskey) -> AuthResult<Passkey> {
681 self.store
682 .create_passkey_with_connection(Exec::tx(self.tx), data)
683 .await
684 }
685 async fn create_user(&self, create_user: CreateUser) -> AuthResult<S::User> {
686 let user = self.store.create_user_in_tx(self.tx, create_user).await?;
687 self.pending_after
688 .push(AfterHook::UserCreated(user.clone()))
689 .await;
690 Ok(user)
691 }
692
693 async fn create_user_prepared(&self, prepared: PreparedUserCreation) -> AuthResult<S::User> {
694 let user = self
695 .store
696 .create_user_prepared_in_tx(self.tx, prepared)
697 .await?;
698 self.pending_after
699 .push(AfterHook::UserCreated(user.clone()))
700 .await;
701 Ok(user)
702 }
703
704 async fn create_account(&self, create_account: CreateAccount) -> AuthResult<S::Account> {
705 let account = self
706 .store
707 .create_account_in_tx(self.tx, create_account)
708 .await?;
709 self.pending_after
710 .push(AfterHook::AccountCreated(account.clone()))
711 .await;
712 Ok(account)
713 }
714
715 async fn prepare_secondary_session_update(
716 &self,
717 session: S::Session,
718 expires_at: Option<DateTime<Utc>>,
719 fields: FieldValues,
720 ) -> AuthResult<Option<(S::Session, FieldValues)>> {
721 self.store
722 .prepare_secondary_update_with_connection(
723 Exec::tx(self.tx),
724 Some(self.tx),
725 session,
726 expires_at,
727 fields,
728 )
729 .await
730 }
731 async fn complete_secondary_session_update(
732 &self,
733 session: S::Session,
734 expires_at: Option<DateTime<Utc>>,
735 fields: FieldValues,
736 persist: bool,
737 ) -> AuthResult<Option<S::Session>> {
738 use alibi_core::AuthSession;
739 let token = session.token().to_owned();
740 let result = self
741 .store
742 .complete_secondary_update_with_connection(
743 Exec::tx(self.tx),
744 Some(self.tx),
745 session,
746 expires_at,
747 fields,
748 persist,
749 )
750 .await?;
751 let event = result.as_ref().map_or_else(
752 || AfterHook::SessionUpdateMissing(token),
753 |model| AfterHook::SessionUpdated(model.clone()),
754 );
755 self.pending_after.push(event).await;
756 Ok(result)
757 }
758 async fn prepare_secondary_session_creation(
759 &self,
760 input: CreateSession,
761 persist: bool,
762 ) -> AuthResult<S::Session> {
763 let session = self
764 .store
765 .prepare_secondary_session_in_tx(self.tx, input, persist)
766 .await?;
767 self.pending_after
768 .push(AfterHook::SessionCreated(session.clone()))
769 .await;
770 Ok(session)
771 }
772 async fn create_session(&self, create_session: CreateSession) -> AuthResult<S::Session> {
773 let session = self
774 .store
775 .create_session_in_tx(self.tx, create_session)
776 .await?;
777 self.pending_after
778 .push(AfterHook::SessionCreated(session.clone()))
779 .await;
780 Ok(session)
781 }
782 async fn create_verification_record(
783 &self,
784 data: VerificationCreation,
785 publication: VerificationPublication,
786 ) -> AuthResult<Option<VerificationSnapshot>> {
787 let snapshot = self
788 .store
789 .create_verification_record_with_connection(
790 Exec::tx(self.tx),
791 Some(self.tx),
792 data,
793 publication,
794 )
795 .await?;
796 if let Some(snapshot) = &snapshot {
797 self.pending_after
798 .push(AfterHook::VerificationRecordCreated(snapshot.clone()))
799 .await;
800 }
801 Ok(snapshot)
802 }
803
804 async fn create_verification(
805 &self,
806 verification: CreateVerification,
807 ) -> AuthResult<S::Verification> {
808 let verification = self
809 .store
810 .create_verification_in_tx(self.tx, verification)
811 .await?;
812 self.pending_after
813 .push(AfterHook::VerificationCreated(verification.clone()))
814 .await;
815 Ok(verification)
816 }
817}
818
819#[async_trait]
820impl<S> TransactionStore<S> for SqlxStore<S>
821where
822 S: AuthSchema,
823 S::User: SqlxUserModel,
824 S::Account: SqlxAccountModel,
825 S::Session: SqlxSessionModel,
826 S::Verification: SqlxVerificationModel,
827{
828 async fn transaction_boxed(
829 &self,
830 work: Box<TransactionWork<S>>,
831 ) -> AuthResult<BoxedTransactionValue> {
832 self.transaction_boxed_with_policy(
833 work,
834 alibi_core::store::AfterCommitErrorPolicy::Propagate,
835 )
836 .await
837 }
838 async fn transaction_boxed_with_policy(
839 &self,
840 work: Box<TransactionWork<S>>,
841 policy: alibi_core::store::AfterCommitErrorPolicy,
842 ) -> AuthResult<BoxedTransactionValue> {
843 let tx = self.begin(false).await?;
844 let tx_store = SqlxStoreTransaction {
845 store: self,
846 tx: &tx,
847 pending_after: AfterHookQueue::new(),
848 };
849 let outcome = work(&tx_store).await;
850 let pending_after = tx_store.pending_after;
851 match outcome {
852 Ok(value) => {
853 tx.commit().await?;
854 pending_after
857 .run_with_policy(self.hooks(), &self.hook_context(None), policy)
858 .await?;
859 Ok(value)
860 }
861 Err(err) => {
862 tx.rollback().await?;
863 Err(err)
864 }
865 }
866 }
867}
868
869pub(crate) fn lock_exclusive(sql: &mut Sql) {
871 if sql.engine() == Engine::Postgres {
872 sql.push(" FOR UPDATE");
873 }
874}
875
876pub(crate) fn lock_shared(sql: &mut Sql) {
878 if sql.engine() == Engine::Postgres {
879 sql.push(" FOR SHARE");
880 }
881}
882
883pub(crate) fn bind_page(sql: &mut crate::sql::Sql, limit: Option<f64>, offset: Option<f64>) {
885 for (clause, number) in [(" LIMIT ", limit), (" OFFSET ", offset)] {
886 if let Some(number) = number {
887 sql.push(clause);
888 if sql.engine() == Engine::Postgres {
889 sql.bind(ryu_js::Buffer::new().format(number).to_owned());
892 sql.push("::bigint");
893 } else {
894 sql.bind(number);
895 }
896 }
897 }
898}
899
900mod sso;
901
902mod scim;
903mod scim_decommission;
904mod scim_groups;
905mod scim_projection;
906mod scim_user_create;
907mod scim_user_replace;
908mod scim_users;