Skip to main content

alibi_core/store/
adapter.rs

1//! Backend-neutral pieces shared by the bundled persistence adapters.
2//!
3//! Everything here is independent of the driver: hook sequencing after a
4//! committed transaction, the error a vetoing hook produces, raw configured
5//! field values, and timestamp parsing for string-dated plugin rows.
6
7use crate::error::{AuthError, AuthResult};
8use crate::schema::AuthSchema;
9use crate::store::{DatabaseHookContext, DatabaseHooks, HookBackend};
10use crate::utils::json::JsValue;
11use crate::verification::VerificationSnapshot;
12use chrono::{DateTime, Utc};
13use std::sync::Arc;
14
15/// The error returned when a `before_*` hook cancels an operation.
16#[must_use]
17pub fn cancelled_by_hook(operation: &str) -> AuthError {
18    AuthError::forbidden(format!("{operation} cancelled by database hook"))
19}
20
21/// Preserve an ordinary application callback exception as an empty HTTP 500.
22/// Explicit API errors retain their public status and body.
23#[must_use]
24pub fn callback_error(error: AuthError) -> AuthError {
25    match error {
26        AuthError::Internal(_) => AuthError::CallbackFailure(Box::new(error)),
27        other => other,
28    }
29}
30
31/// Parse an RFC 3339 timestamp of a string-dated plugin field.
32///
33/// # Errors
34///
35/// Returns a bad-request error naming `field` when the text is not RFC 3339.
36pub fn parse_rfc3339(value: &str, field: &str) -> AuthResult<DateTime<Utc>> {
37    DateTime::parse_from_rfc3339(value)
38        .map(|dt| dt.with_timezone(&Utc))
39        .map_err(|_| AuthError::bad_request(format!("Invalid RFC 3339 timestamp for {field}")))
40}
41
42/// [`parse_rfc3339`] over an optional value.
43///
44/// # Errors
45///
46/// Returns a bad-request error naming `field` when the text is not RFC 3339.
47pub fn parse_optional_rfc3339(
48    value: Option<&str>,
49    field: &str,
50) -> AuthResult<Option<DateTime<Utc>>> {
51    value.map(|inner| parse_rfc3339(inner, field)).transpose()
52}
53
54/// An after-hook deferred until the enclosing auth transaction commits.
55pub enum AfterHook<S: AuthSchema> {
56    UserCreated(S::User),
57    AccountCreated(S::Account),
58    SessionCreated(S::Session),
59    SessionUpdated(S::Session),
60    SessionUpdateMissing(String),
61    VerificationCreated(S::Verification),
62    VerificationRecordCreated(VerificationSnapshot),
63}
64
65impl<S: AuthSchema> AfterHook<S> {
66    /// Invoke the matching callback on one hook.
67    ///
68    /// # Errors
69    ///
70    /// Returns the hook's error.
71    pub async fn dispatch<B: HookBackend>(
72        &self,
73        hook: &dyn DatabaseHooks<S, B>,
74        ctx: &DatabaseHookContext<'_, B>,
75    ) -> AuthResult<()> {
76        match self {
77            Self::UserCreated(user) => hook.after_create_user(user, ctx).await,
78            Self::AccountCreated(account) => hook.after_create_account(account, ctx).await,
79            Self::SessionCreated(session) => hook
80                .after_create_session(session, ctx)
81                .await
82                .map_err(callback_error),
83            Self::SessionUpdated(session) => hook.after_update_session(session, ctx).await,
84            Self::SessionUpdateMissing(token) => {
85                hook.after_update_session_missing(token, ctx).await
86            }
87            Self::VerificationCreated(verification) => {
88                hook.after_create_verification(verification, ctx).await
89            }
90            Self::VerificationRecordCreated(snapshot) => {
91                hook.after_create_verification_record(snapshot, ctx).await
92            }
93        }
94    }
95}
96
97/// After-hooks recorded by writes inside an auth transaction.
98///
99/// Rollback drops the queue. After commit, [`run`](Self::run) replays every
100/// event in operation order, invoking each registered hook per event; a hook
101/// error is returned to the caller after the auth writes have committed.
102pub struct AfterHookQueue<S: AuthSchema>(tokio::sync::Mutex<Vec<AfterHook<S>>>);
103
104impl<S: AuthSchema> Default for AfterHookQueue<S> {
105    fn default() -> Self {
106        Self::new()
107    }
108}
109
110impl<S: AuthSchema> AfterHookQueue<S> {
111    #[must_use]
112    pub const fn new() -> Self {
113        Self(tokio::sync::Mutex::const_new(Vec::new()))
114    }
115
116    pub async fn push(&self, event: AfterHook<S>) {
117        self.0.lock().await.push(event);
118    }
119
120    /// Run every queued event against `hooks`, in order.
121    ///
122    /// # Errors
123    ///
124    /// Returns the first hook error.
125    pub async fn run<B: HookBackend>(
126        self,
127        hooks: &[Arc<dyn DatabaseHooks<S, B>>],
128        ctx: &DatabaseHookContext<'_, B>,
129    ) -> AuthResult<()> {
130        for event in self.0.into_inner() {
131            for hook in hooks {
132                event.dispatch(hook.as_ref(), ctx).await?;
133            }
134        }
135        Ok(())
136    }
137}
138
139/// A configured additional-field value before backend coercion.
140///
141/// JavaScript numbers that are finite integers within `i64` range bind as
142/// integers so SQLite keeps INTEGER affinity; `-0` and fractions stay doubles.
143#[derive(Clone, Debug, PartialEq)]
144pub enum RawFieldValue {
145    Null,
146    Bool(bool),
147    Integer(i64),
148    Number(f64),
149    Text(String),
150    Json(serde_json::Value),
151}
152
153impl RawFieldValue {
154    /// # Errors
155    ///
156    /// Returns an error if an object or array cannot be serialized as JSON.
157    #[expect(
158        clippy::as_conversions,
159        clippy::cast_possible_truncation,
160        reason = "the range guard above makes the integer coercion exact"
161    )]
162    pub fn from_js(value: &JsValue) -> Result<Self, serde_json::Error> {
163        Ok(match value {
164            JsValue::Null => Self::Null,
165            JsValue::Bool(value) => Self::Bool(*value),
166            JsValue::Number(value)
167                if value.is_finite()
168                    && value.fract() == 0.0
169                    && !(*value == 0.0 && value.is_sign_negative())
170                    && (-9_223_372_036_854_776_000.0..9_223_372_036_854_776_000.0)
171                        .contains(value) =>
172            {
173                Self::Integer(*value as i64)
174            }
175            JsValue::Number(value) => Self::Number(*value),
176            JsValue::String(value) => Self::Text(value.clone()),
177            JsValue::Array(_) | JsValue::Object(_) => Self::Json(value.to_json_value()?),
178        })
179    }
180}