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 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 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
97pub 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 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#[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 #[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}