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 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 #[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 i64::try_from(self.config.advanced.database.default_find_many_limit).unwrap_or(i64::MAX)
130 }
131
132 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#[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 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
435pub(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
442pub(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
449pub(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 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;