1use 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 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}
71
72impl<S: AuthSchema> AfterHook<S> {
73 pub async fn dispatch<B: HookBackend>(
79 &self,
80 hook: &dyn DatabaseHooks<S, B>,
81 ctx: &DatabaseHookContext<'_, B>,
82 ) -> AuthResult<()> {
83 match self {
84 Self::UserCreated(user) => hook.after_create_user(user, ctx).await,
85 Self::UserUpdated(user) => hook
86 .after_update_user(user, ctx)
87 .await
88 .map_err(callback_error),
89 Self::AccountCreated(account) => hook.after_create_account(account, ctx).await,
90 Self::AccountUpdated(account) => hook.after_update_account(account, ctx).await,
91 Self::SessionCreated(session) => hook
92 .after_create_session(session, ctx)
93 .await
94 .map_err(callback_error),
95 Self::SessionUpdated(session) => hook.after_update_session(session, ctx).await,
96 Self::SessionUpdateMissing(token) => {
97 hook.after_update_session_missing(token, ctx).await
98 }
99 Self::SessionDeleted(session) => hook.after_delete_session(session, ctx).await,
100 Self::SessionRetirement { .. } => Ok(()),
101 Self::VerificationCreated(verification) => {
102 hook.after_create_verification(verification, ctx).await
103 }
104 Self::VerificationRecordCreated(snapshot) => {
105 hook.after_create_verification_record(snapshot, ctx).await
106 }
107 }
108 }
109}
110
111pub struct AfterHookQueue<S: AuthSchema>(tokio::sync::Mutex<Vec<AfterHook<S>>>);
117
118impl<S: AuthSchema> Default for AfterHookQueue<S> {
119 fn default() -> Self {
120 Self::new()
121 }
122}
123
124impl<S: AuthSchema> AfterHookQueue<S> {
125 #[must_use]
126 pub const fn new() -> Self {
127 Self(tokio::sync::Mutex::const_new(Vec::new()))
128 }
129
130 pub async fn push(&self, event: AfterHook<S>) {
131 self.0.lock().await.push(event);
132 }
133 pub async fn attach_session_retirement(
135 &self,
136 session_id: &str,
137 works: Vec<Box<dyn crate::store::SessionDeletionWork<S>>>,
138 context: Arc<crate::AuthContext<S>>,
139 ) {
140 use crate::AuthSession as _;
141 let mut events = self.0.lock().await;
142 if let Some(index) = events.iter().rposition(|event| matches!(event, AfterHook::SessionDeleted(session) if session.id().as_ref() == session_id)) {
143 events.insert(index + 1, AfterHook::SessionRetirement { works, context });
144 }
145 }
146
147 pub async fn run<B: HookBackend>(
153 self,
154 hooks: &[Arc<dyn DatabaseHooks<S, B>>],
155 ctx: &DatabaseHookContext<'_, B>,
156 ) -> AuthResult<()> {
157 self.run_with_policy(hooks, ctx, crate::store::AfterCommitErrorPolicy::Propagate)
158 .await
159 }
160 pub async fn run_with_policy<B: HookBackend>(
165 self,
166 hooks: &[Arc<dyn DatabaseHooks<S, B>>],
167 ctx: &DatabaseHookContext<'_, B>,
168 policy: crate::store::AfterCommitErrorPolicy,
169 ) -> AuthResult<()> {
170 for event in self.0.into_inner() {
171 if let AfterHook::SessionRetirement { works, context } = event {
172 for work in works {
173 if let Err(error) = work.complete(&context).await {
174 if policy == crate::store::AfterCommitErrorPolicy::Propagate {
175 return Err(error);
176 }
177 tracing::error!("Committed session retirement failed");
178 }
179 }
180 continue;
181 }
182 for hook in hooks {
183 if let Err(error) = event.dispatch(hook.as_ref(), ctx).await {
184 if policy == crate::store::AfterCommitErrorPolicy::Propagate {
185 return Err(error);
186 }
187 tracing::error!("Committed transaction after-hook failed");
188 }
189 }
190 }
191 Ok(())
192 }
193}
194
195#[derive(Clone, Debug, PartialEq)]
200pub enum RawFieldValue {
201 Null,
202 Bool(bool),
203 Integer(i64),
204 Number(f64),
205 Text(String),
206 Json(serde_json::Value),
207}
208
209impl RawFieldValue {
210 #[expect(
214 clippy::as_conversions,
215 clippy::cast_possible_truncation,
216 reason = "the range guard above makes the integer coercion exact"
217 )]
218 pub fn from_js(value: &JsValue) -> Result<Self, serde_json::Error> {
219 Ok(match value {
220 JsValue::Null => Self::Null,
221 JsValue::Bool(value) => Self::Bool(*value),
222 JsValue::Number(value)
223 if value.is_finite()
224 && value.fract() == 0.0
225 && !(*value == 0.0 && value.is_sign_negative())
226 && (-9_223_372_036_854_776_000.0..9_223_372_036_854_776_000.0)
227 .contains(value) =>
228 {
229 Self::Integer(*value as i64)
230 }
231 JsValue::Number(value) => Self::Number(*value),
232 JsValue::String(value) => Self::Text(value.clone()),
233 JsValue::Date(value) => {
234 Self::Text(value.to_rfc3339_opts(chrono::SecondsFormat::Millis, true))
235 }
236 JsValue::Array(_) | JsValue::Object(_) => Self::Json(value.to_json_value()?),
237 })
238 }
239}