alibi_core/store/
adapter.rs1use 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#[must_use]
17pub fn cancelled_by_hook(operation: &str) -> AuthError {
18 AuthError::forbidden(format!("{operation} cancelled by database hook"))
19}
20
21#[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
31pub 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
42pub 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
54pub 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 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
104pub 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 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 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#[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 #[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}