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    UserUpdated(S::User),
58    AccountCreated(S::Account),
59    AccountUpdated(S::Account),
60    SessionCreated(S::Session),
61    SessionUpdated(S::Session),
62    SessionUpdateMissing(String),
63    VerificationCreated(S::Verification),
64    VerificationRecordCreated(VerificationSnapshot),
65}
66
67impl<S: AuthSchema> AfterHook<S> {
68    /// Invoke the matching callback on one hook.
69    ///
70    /// # Errors
71    ///
72    /// Returns the hook's error.
73    pub async fn dispatch<B: HookBackend>(
74        &self,
75        hook: &dyn DatabaseHooks<S, B>,
76        ctx: &DatabaseHookContext<'_, B>,
77    ) -> AuthResult<()> {
78        match self {
79            Self::UserCreated(user) => hook.after_create_user(user, ctx).await,
80            Self::UserUpdated(user) => hook
81                .after_update_user(user, ctx)
82                .await
83                .map_err(callback_error),
84            Self::AccountCreated(account) => hook.after_create_account(account, ctx).await,
85            Self::AccountUpdated(account) => hook.after_update_account(account, ctx).await,
86            Self::SessionCreated(session) => hook
87                .after_create_session(session, ctx)
88                .await
89                .map_err(callback_error),
90            Self::SessionUpdated(session) => hook.after_update_session(session, ctx).await,
91            Self::SessionUpdateMissing(token) => {
92                hook.after_update_session_missing(token, ctx).await
93            }
94            Self::VerificationCreated(verification) => {
95                hook.after_create_verification(verification, ctx).await
96            }
97            Self::VerificationRecordCreated(snapshot) => {
98                hook.after_create_verification_record(snapshot, ctx).await
99            }
100        }
101    }
102}
103
104/// After-hooks recorded by writes inside an auth transaction.
105///
106/// Rollback drops the queue. After commit, [`run`](Self::run) replays every
107/// event in operation order, invoking each registered hook per event; a hook
108/// error is returned to the caller after the auth writes have committed.
109pub struct AfterHookQueue<S: AuthSchema>(tokio::sync::Mutex<Vec<AfterHook<S>>>);
110
111impl<S: AuthSchema> Default for AfterHookQueue<S> {
112    fn default() -> Self {
113        Self::new()
114    }
115}
116
117impl<S: AuthSchema> AfterHookQueue<S> {
118    #[must_use]
119    pub const fn new() -> Self {
120        Self(tokio::sync::Mutex::const_new(Vec::new()))
121    }
122
123    pub async fn push(&self, event: AfterHook<S>) {
124        self.0.lock().await.push(event);
125    }
126
127    /// Run every queued event against `hooks`, in order.
128    ///
129    /// # Errors
130    ///
131    /// Returns the first hook error.
132    pub async fn run<B: HookBackend>(
133        self,
134        hooks: &[Arc<dyn DatabaseHooks<S, B>>],
135        ctx: &DatabaseHookContext<'_, B>,
136    ) -> AuthResult<()> {
137        self.run_with_policy(hooks, ctx, crate::store::AfterCommitErrorPolicy::Propagate)
138            .await
139    }
140    /// Run queued hooks, optionally reporting errors after durable commit.
141    ///
142    /// # Errors
143    /// Returns hook errors when propagation is selected.
144    pub async fn run_with_policy<B: HookBackend>(
145        self,
146        hooks: &[Arc<dyn DatabaseHooks<S, B>>],
147        ctx: &DatabaseHookContext<'_, B>,
148        policy: crate::store::AfterCommitErrorPolicy,
149    ) -> AuthResult<()> {
150        for event in self.0.into_inner() {
151            for hook in hooks {
152                if let Err(error) = event.dispatch(hook.as_ref(), ctx).await {
153                    if policy == crate::store::AfterCommitErrorPolicy::Propagate {
154                        return Err(error);
155                    }
156                    tracing::error!("Committed transaction after-hook failed");
157                }
158            }
159        }
160        Ok(())
161    }
162}
163
164/// A configured additional-field value before backend coercion.
165///
166/// JavaScript numbers that are finite integers within `i64` range bind as
167/// integers so SQLite keeps INTEGER affinity; `-0` and fractions stay doubles.
168#[derive(Clone, Debug, PartialEq)]
169pub enum RawFieldValue {
170    Null,
171    Bool(bool),
172    Integer(i64),
173    Number(f64),
174    Text(String),
175    Json(serde_json::Value),
176}
177
178impl RawFieldValue {
179    /// # Errors
180    ///
181    /// Returns an error if an object or array cannot be serialized as JSON.
182    #[expect(
183        clippy::as_conversions,
184        clippy::cast_possible_truncation,
185        reason = "the range guard above makes the integer coercion exact"
186    )]
187    pub fn from_js(value: &JsValue) -> Result<Self, serde_json::Error> {
188        Ok(match value {
189            JsValue::Null => Self::Null,
190            JsValue::Bool(value) => Self::Bool(*value),
191            JsValue::Number(value)
192                if value.is_finite()
193                    && value.fract() == 0.0
194                    && !(*value == 0.0 && value.is_sign_negative())
195                    && (-9_223_372_036_854_776_000.0..9_223_372_036_854_776_000.0)
196                        .contains(value) =>
197            {
198                Self::Integer(*value as i64)
199            }
200            JsValue::Number(value) => Self::Number(*value),
201            JsValue::String(value) => Self::Text(value.clone()),
202            JsValue::Date(value) => {
203                Self::Text(value.to_rfc3339_opts(chrono::SecondsFormat::Millis, true))
204            }
205            JsValue::Array(_) | JsValue::Object(_) => Self::Json(value.to_json_value()?),
206        })
207    }
208}