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