1use super::{SqlxStore, lock_exclusive};
2use crate::model::{self, SqlxModel};
3use crate::pool::{Exec, SqlxTransaction};
4use crate::schema::{AuthSchema, SqlxVerificationModel};
5use crate::sql::Sql;
6use crate::value::SqlValue;
7use alibi_core::entity::AuthVerification;
8use alibi_core::error::{AuthError, AuthResult};
9use alibi_core::store::adapter::cancelled_by_hook;
10use alibi_core::store::{VerificationStore, verification_reservation_id};
11use alibi_core::types::{CreateVerification, UpdateVerification};
12use alibi_core::verification::{
13 VerificationCreation, VerificationPublication, VerificationSnapshot,
14};
15use async_trait::async_trait;
16use chrono::{DateTime, SubsecRound, Utc};
17
18impl<S> SqlxStore<S>
19where
20 S: AuthSchema,
21 S::Verification: SqlxVerificationModel,
22{
23 fn verification_table() -> &'static str {
24 <S::Verification as SqlxModel>::TABLE
25 }
26
27 fn unexpired() -> (&'static str, &'static str, SqlValue) {
29 (
30 S::Verification::expires_at_column(),
31 " > ",
32 S::Verification::timestamp_value(S::Verification::expires_at_column(), Utc::now()),
33 )
34 }
35
36 pub(crate) async fn create_verification_record_with_connection(
37 &self,
38 exec: Exec<'_>,
39 tx: Option<&SqlxTransaction>,
40 mut data: VerificationCreation,
41 publication: VerificationPublication,
42 ) -> AuthResult<Option<VerificationSnapshot>> {
43 let hook_context = self.hook_context(tx);
44 for hook in self.hooks() {
45 if hook
46 .before_create_verification_record(&mut data, &hook_context)
47 .await?
48 .is_cancelled()
49 {
50 return Ok(None);
51 }
52 }
53 let snapshot = if publication.store_in_database {
54 if data.id.is_none() {
55 data.id = self
56 .generated_id(
57 exec,
58 "verification",
59 Self::verification_table(),
60 S::Verification::id_column(),
61 )
62 .await?;
63 }
64 let id = data
65 .id
66 .as_deref()
67 .map(S::Verification::parse_id)
68 .transpose()?;
69 let mut active = S::Verification::new_active(id, data.data(), data.created_at);
70 if let Some(updated) = S::Verification::updated_at_column() {
71 let column = <S::Verification as SqlxModel>::COLUMNS
72 .iter()
73 .find(|column| column.name == updated)
74 .ok_or_else(|| {
75 AuthError::internal(
76 "the verification update timestamp binding has no column",
77 )
78 })?;
79 active.set(
80 column.name,
81 S::Verification::timestamp_value(column.name, data.updated_at),
82 );
83 } else if data.updated_at != data.created_at {
84 return Err(AuthError::internal(
85 "the verification schema cannot preserve a distinct update timestamp",
86 ));
87 }
88 let model = model::insert::<S::Verification>(exec, &active).await?;
89 VerificationSnapshot::from_model(&model)
90 } else {
91 data.snapshot()
92 };
93 publication.publish(&snapshot).await?;
94 if tx.is_none() {
95 for hook in self.hooks() {
96 hook.after_create_verification_record(&snapshot, &hook_context)
97 .await?;
98 }
99 }
100 Ok(Some(snapshot))
101 }
102
103 async fn create_verification_with_connection(
104 &self,
105 exec: Exec<'_>,
106 tx: Option<&SqlxTransaction>,
107 mut verification: CreateVerification,
108 ) -> AuthResult<S::Verification> {
109 let hook_context = self.hook_context(tx);
110 for hook in self.hooks() {
111 if hook
112 .before_create_verification(&mut verification, &hook_context)
113 .await?
114 .is_cancelled()
115 {
116 return Err(cancelled_by_hook("verification creation"));
117 }
118 }
119 let generated_id = self
120 .generated_id(
121 exec,
122 "verification",
123 Self::verification_table(),
124 S::Verification::id_column(),
125 )
126 .await?;
127 let id = generated_id
128 .as_deref()
129 .map(S::Verification::parse_id)
130 .transpose()?;
131 let active = S::Verification::new_active(id, verification, Utc::now());
132 let verification = model::insert::<S::Verification>(exec, &active).await?;
133 if tx.is_none() {
134 for hook in self.hooks() {
135 hook.after_create_verification(&verification, &hook_context)
136 .await?;
137 }
138 }
139 Ok(verification)
140 }
141
142 pub(crate) async fn create_verification_in_tx(
143 &self,
144 tx: &SqlxTransaction,
145 verification: CreateVerification,
146 ) -> AuthResult<S::Verification> {
147 self.create_verification_with_connection(Exec::tx(tx), Some(tx), verification)
148 .await
149 }
150
151 fn newest_generation(
153 exec: Exec<'_>,
154 filters: &[(&'static str, &'static str, SqlValue)],
155 ) -> Sql {
156 let table = Self::verification_table();
157 let mut sql = model::select_model::<S::Verification>(exec);
158 for (index, (column, operator, value)) in filters.iter().enumerate() {
159 sql.push(if index == 0 { " WHERE " } else { " AND " });
160 sql.column(table, column);
161 sql.push(operator);
162 sql.bind(S::Verification::column_value(column, value.clone()));
163 }
164 sql.push(" ORDER BY ");
165 sql.column(table, S::Verification::created_at_column());
166 sql.push(" DESC LIMIT 1");
167 sql
168 }
169
170 async fn consume_verification_generation(
171 &self,
172 identifier: &str,
173 expected_value: Option<&str>,
174 ) -> AuthResult<Option<S::Verification>> {
175 let transaction = self.begin(true).await?;
180 let outcome = self
181 .consume_generation_in(&transaction, identifier, expected_value)
182 .await;
183 let consumed = match outcome {
184 Ok(consumed) => {
185 transaction.commit().await?;
186 consumed
187 }
188 Err(error) => {
189 transaction.rollback().await?;
190 return Err(error);
191 }
192 };
193 if let Some(model) = &consumed {
194 let hook_context = self.hook_context(None);
195 for hook in self.hooks() {
196 hook.after_delete_verification(model, &hook_context).await?;
197 }
198 }
199 Ok(consumed)
202 }
203
204 pub(crate) async fn consume_generation_in(
207 &self,
208 transaction: &SqlxTransaction,
209 identifier: &str,
210 expected_value: Option<&str>,
211 ) -> AuthResult<Option<S::Verification>> {
212 let exec = Exec::tx(transaction);
213 let mut select = Self::newest_generation(
214 exec,
215 &[(
216 S::Verification::identifier_column(),
217 " = ",
218 identifier.into(),
219 )],
220 );
221 lock_exclusive(&mut select);
222 let Some(model) = exec.fetch_optional::<S::Verification>(select).await? else {
223 return Ok(None);
224 };
225 if expected_value.is_some_and(|expected| model.value() != expected) {
226 return Ok(None);
227 }
228 let hook_context = self.hook_context(Some(transaction));
229 for hook in self.hooks() {
230 if hook
231 .before_delete_verification(&model, &hook_context)
232 .await?
233 .is_cancelled()
234 {
235 return Ok(None);
237 }
238 }
239 let table = Self::verification_table();
240 let id = S::Verification::parse_id(model.id().as_ref())?;
241 let mut delete = Sql::with(exec.engine(), "DELETE FROM ");
242 delete.ident(table);
243 delete.push(" WHERE ");
244 delete.compare_model::<S::Verification>(table, S::Verification::id_column(), " = ", id);
245 delete.push(" AND ");
246 delete.compare_model::<S::Verification>(
247 table,
248 S::Verification::value_column(),
249 " = ",
250 model.value(),
251 );
252 if exec.execute(delete).await? != 1 {
253 return Ok(None);
254 }
255 let mut siblings = Sql::with(exec.engine(), "DELETE FROM ");
256 siblings.ident(table);
257 siblings.push(" WHERE ");
258 siblings.compare_model::<S::Verification>(
259 table,
260 S::Verification::identifier_column(),
261 " = ",
262 identifier,
263 );
264 _ = exec.execute(siblings).await?;
265 Ok(Some(model))
266 }
267
268 async fn update_verifications_returning(
271 &self,
272 filters: &[(&'static str, SqlValue)],
273 value: Option<String>,
274 expires_at: Option<DateTime<Utc>>,
275 updated_at_column: &'static str,
276 ) -> AuthResult<Option<S::Verification>> {
277 let table = Self::verification_table();
278 let mut sql = Sql::with(self.exec().engine(), "UPDATE ");
279 sql.ident(table);
280 sql.push(" SET ");
281 sql.assign(
282 updated_at_column,
283 S::Verification::timestamp_value(updated_at_column, Utc::now()),
284 );
285 if let Some(value) = value {
286 sql.push(", ");
287 sql.assign(
288 S::Verification::value_column(),
289 S::Verification::column_value(S::Verification::value_column(), value.into()),
290 );
291 }
292 if let Some(expires_at) = expires_at {
293 sql.push(", ");
294 sql.assign(
295 S::Verification::expires_at_column(),
296 S::Verification::timestamp_value(S::Verification::expires_at_column(), expires_at),
297 );
298 }
299 for (index, (column, value)) in filters.iter().enumerate() {
300 sql.push(if index == 0 { " WHERE " } else { " AND " });
301 sql.compare_model::<S::Verification>(table, column, " = ", value.clone());
302 }
303 model::returning::<S::Verification>(&mut sql);
304 self.exec().fetch_optional::<S::Verification>(sql).await
305 }
306}
307
308#[async_trait]
309impl<S> VerificationStore<S> for SqlxStore<S>
310where
311 S: AuthSchema + Send + Sync,
312 S::Verification: SqlxVerificationModel,
313{
314 async fn create_verification_record(
315 &self,
316 data: VerificationCreation,
317 publication: VerificationPublication,
318 ) -> AuthResult<Option<VerificationSnapshot>> {
319 self.create_verification_record_with_connection(self.exec(), None, data, publication)
320 .await
321 }
322
323 async fn create_verification(
324 &self,
325 verification: CreateVerification,
326 ) -> AuthResult<S::Verification> {
327 self.create_verification_with_connection(self.exec(), None, verification)
328 .await
329 }
330
331 async fn get_verification(
332 &self,
333 identifier: &str,
334 value: &str,
335 ) -> AuthResult<Option<S::Verification>> {
336 let sql = Self::newest_generation(
337 self.exec(),
338 &[
339 (
340 S::Verification::identifier_column(),
341 " = ",
342 identifier.into(),
343 ),
344 (S::Verification::value_column(), " = ", value.into()),
345 Self::unexpired(),
346 ],
347 );
348 self.exec().fetch_optional(sql).await
349 }
350
351 async fn get_verification_by_value(&self, value: &str) -> AuthResult<Option<S::Verification>> {
352 let sql = Self::newest_generation(
353 self.exec(),
354 &[
355 (S::Verification::value_column(), " = ", value.into()),
356 Self::unexpired(),
357 ],
358 );
359 self.exec().fetch_optional(sql).await
360 }
361
362 async fn get_verification_by_identifier(
363 &self,
364 identifier: &str,
365 ) -> AuthResult<Option<S::Verification>> {
366 let sql = Self::newest_generation(
367 self.exec(),
368 &[
369 (
370 S::Verification::identifier_column(),
371 " = ",
372 identifier.into(),
373 ),
374 Self::unexpired(),
375 ],
376 );
377 self.exec().fetch_optional(sql).await
378 }
379
380 async fn consume_verification(
381 &self,
382 identifier: &str,
383 value: &str,
384 ) -> AuthResult<Option<S::Verification>> {
385 Ok(self
386 .consume_verification_generation(identifier, Some(value))
387 .await?
388 .filter(|model| model.expires_at() >= Utc::now()))
389 }
390
391 async fn get_latest_verification_by_identifier(
392 &self,
393 identifier: &str,
394 ) -> AuthResult<Option<S::Verification>> {
395 let sql = Self::newest_generation(
396 self.exec(),
397 &[(
398 S::Verification::identifier_column(),
399 " = ",
400 identifier.into(),
401 )],
402 );
403 self.exec().fetch_optional(sql).await
404 }
405
406 async fn consume_verification_by_identifier(
407 &self,
408 identifier: &str,
409 ) -> AuthResult<Option<S::Verification>> {
410 Ok(self
411 .consume_verification_generation(identifier, None)
412 .await?
413 .filter(|model| model.expires_at() >= Utc::now()))
414 }
415
416 async fn consume_verification_snapshot(
417 &self,
418 identifier: &str,
419 ) -> AuthResult<Option<S::Verification>> {
420 self.consume_verification_generation(identifier, None).await
421 }
422
423 async fn delete_verifications_by_identifier(&self, identifier: &str) -> AuthResult<()> {
424 let table = Self::verification_table();
425 let mut select = model::select_model::<S::Verification>(self.exec());
426 select.push(" WHERE ");
427 select.compare_model::<S::Verification>(
428 table,
429 S::Verification::identifier_column(),
430 " = ",
431 identifier,
432 );
433 select.push(" LIMIT 1");
434 let Some(model) = self
435 .exec()
436 .fetch_optional::<S::Verification>(select)
437 .await?
438 else {
439 return Ok(());
440 };
441 let hook_context = self.hook_context(None);
445 for hook in self.hooks() {
446 if hook
447 .before_delete_verification(&model, &hook_context)
448 .await?
449 .is_cancelled()
450 {
451 return Ok(());
452 }
453 }
454 let mut delete = Sql::with(self.exec().engine(), "DELETE FROM ");
455 delete.ident(table);
456 delete.push(" WHERE ");
457 delete.compare_model::<S::Verification>(
458 table,
459 S::Verification::identifier_column(),
460 " = ",
461 identifier,
462 );
463 _ = self.exec().execute(delete).await?;
464 for hook in self.hooks() {
465 hook.after_delete_verification(&model, &hook_context)
466 .await?;
467 }
468 Ok(())
469 }
470
471 async fn compare_and_swap_verification(
472 &self,
473 id: &str,
474 expected_value: &str,
475 value: &str,
476 expires_at: DateTime<Utc>,
477 ) -> AuthResult<bool> {
478 let updated_at_column = S::Verification::updated_at_column().ok_or_else(|| {
479 AuthError::internal("the verification schema has no update timestamp binding")
480 })?;
481 let parsed_id = S::Verification::parse_id(id)?;
482 let mut update = UpdateVerification {
483 value: Some(value.to_owned()),
484 expires_at: Some(expires_at),
485 };
486 let hook_context = self.hook_context(None);
487 for hook in self.hooks() {
488 if hook
489 .before_update_verification(id, &mut update, &hook_context)
490 .await?
491 .is_cancelled()
492 {
493 return Ok(false);
494 }
495 }
496 let Some(model) = self
499 .update_verifications_returning(
500 &[
501 (S::Verification::id_column(), parsed_id),
502 (S::Verification::value_column(), expected_value.into()),
503 ],
504 update.value,
505 update.expires_at,
506 updated_at_column,
507 )
508 .await?
509 else {
510 return Ok(false);
511 };
512 for hook in self.hooks() {
513 hook.after_update_verification(&model, &hook_context)
514 .await?;
515 }
516 Ok(true)
517 }
518
519 async fn reserve_verification(&self, verification: CreateVerification) -> AuthResult<bool> {
520 let logical = verification.identifier.clone();
521 Ok(self
522 .reserve_verification_record(&logical, verification)
523 .await?
524 .is_some())
525 }
526
527 async fn reserve_verification_record(
528 &self,
529 logical_identifier: &str,
530 verification: CreateVerification,
531 ) -> AuthResult<Option<S::Verification>> {
532 let (encoded, digest) =
533 verification_reservation_id(logical_identifier, &self.config.advanced.database);
534 let id = S::Verification::parse_reservation_id(&encoded, digest)?;
535 let active = S::Verification::new_active(Some(id.clone()), verification, Utc::now());
536 match model::insert::<S::Verification>(self.exec(), &active).await {
537 Ok(model) => Ok(Some(model)),
538 Err(error) => {
539 let table = Self::verification_table();
540 let mut existing = model::select_model::<S::Verification>(self.exec());
541 existing.push(" WHERE ");
542 existing.compare_model::<S::Verification>(
543 table,
544 S::Verification::id_column(),
545 " = ",
546 id,
547 );
548 existing.push(" LIMIT 1");
549 if self
550 .exec()
551 .fetch_optional::<S::Verification>(existing)
552 .await?
553 .is_some()
554 {
555 Ok(None)
556 } else {
557 Err(error)
558 }
559 }
560 }
561 }
562
563 async fn update_verification_by_identifier(
564 &self,
565 identifier: &str,
566 data: UpdateVerification,
567 ) -> AuthResult<Option<VerificationSnapshot>> {
568 let hook_context = self.hook_context(None);
569 let mut admitted = data.clone();
570 for hook in self.hooks() {
571 let mut candidate = data.clone();
574 if hook
575 .before_update_verification(identifier, &mut candidate, &hook_context)
576 .await?
577 .is_cancelled()
578 {
579 return Ok(None);
580 }
581 if candidate.value.is_some() {
582 admitted.value = candidate.value;
583 }
584 if candidate.expires_at.is_some() {
585 admitted.expires_at = candidate.expires_at;
586 }
587 }
588 let updated_at = S::Verification::updated_at_column().ok_or_else(|| {
589 AuthError::NotImplemented(
590 "Verification update snapshots require an updatedAt column".into(),
591 )
592 })?;
593 let model = self
594 .update_verifications_returning(
595 &[(S::Verification::identifier_column(), identifier.into())],
596 admitted.value,
597 admitted.expires_at,
598 updated_at,
599 )
600 .await?;
601 let snapshot = model.as_ref().map(VerificationSnapshot::from_model);
602 for hook in self.hooks() {
603 hook.after_update_verification_record(snapshot.as_ref(), &hook_context)
604 .await?;
605 }
606 Ok(snapshot)
607 }
608
609 async fn delete_verification(&self, id: &str) -> AuthResult<()> {
610 let verification_id = <S::Verification as SqlxVerificationModel>::parse_id(id)?;
611 let table = Self::verification_table();
612 let mut select = model::select_model::<S::Verification>(self.exec());
613 select.push(" WHERE ");
614 select.compare_model::<S::Verification>(
615 table,
616 S::Verification::id_column(),
617 " = ",
618 verification_id.clone(),
619 );
620 select.push(" LIMIT 1");
621 let verification = self
622 .exec()
623 .fetch_optional::<S::Verification>(select)
624 .await?;
625 let hook_context = self.hook_context(None);
626 if let Some(verification) = &verification {
627 for hook in self.hooks() {
628 if hook
629 .before_delete_verification(verification, &hook_context)
630 .await?
631 .is_cancelled()
632 {
633 return Err(cancelled_by_hook("verification deletion"));
634 }
635 }
636 }
637 let mut delete = Sql::with(self.exec().engine(), "DELETE FROM ");
638 delete.ident(table);
639 delete.push(" WHERE ");
640 delete.compare_model::<S::Verification>(
641 table,
642 S::Verification::id_column(),
643 " = ",
644 verification_id,
645 );
646 _ = self.exec().execute(delete).await?;
647 if let Some(verification) = &verification {
648 for hook in self.hooks() {
649 hook.after_delete_verification(verification, &hook_context)
650 .await?;
651 }
652 }
653 Ok(())
654 }
655
656 async fn delete_expired_verifications(&self) -> AuthResult<usize> {
657 let deadline = Utc::now().trunc_subsecs(3);
660 let table = Self::verification_table();
661 let mut select = model::select_model::<S::Verification>(self.exec());
662 select.push(" WHERE ");
663 select.compare_model::<S::Verification>(
664 table,
665 S::Verification::expires_at_column(),
666 " < ",
667 S::Verification::timestamp_value(S::Verification::expires_at_column(), deadline),
668 );
669 select.push(" LIMIT ");
670 select.bind(self.find_many_limit());
671 let snapshots: Vec<S::Verification> = self.exec().fetch_all(select).await?;
672 let hook_context = self.hook_context(None);
673 for model in &snapshots {
677 for hook in self.hooks() {
678 if hook
679 .before_delete_verification(model, &hook_context)
680 .await?
681 .is_cancelled()
682 {
683 return Ok(0);
684 }
685 }
686 }
687 let mut delete = Sql::with(self.exec().engine(), "DELETE FROM ");
688 delete.ident(table);
689 delete.push(" WHERE ");
690 delete.compare_model::<S::Verification>(
691 table,
692 S::Verification::expires_at_column(),
693 " < ",
694 S::Verification::timestamp_value(S::Verification::expires_at_column(), deadline),
695 );
696 let deleted = self.exec().execute(delete).await?;
697 for model in &snapshots {
698 for hook in self.hooks() {
699 hook.after_delete_verification(model, &hook_context).await?;
700 }
701 }
702 usize::try_from(deleted)
703 .map_err(|_error| AuthError::internal("Verification cleanup count overflow"))
704 }
705}