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 VerificationDeleted(S::Verification),
71}
72
73impl<S: AuthSchema> AfterHook<S> {
74 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
115pub 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 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 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 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#[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 #[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}