use crate::error::{AuthError, AuthResult};
use crate::schema::AuthSchema;
use crate::store::{DatabaseHookContext, DatabaseHooks, HookBackend};
use crate::utils::json::JsValue;
use crate::verification::VerificationSnapshot;
use chrono::{DateTime, Utc};
use std::sync::Arc;
#[must_use]
pub fn cancelled_by_hook(operation: &str) -> AuthError {
AuthError::forbidden(format!("{operation} cancelled by database hook"))
}
#[must_use]
pub fn callback_error(error: AuthError) -> AuthError {
match error {
AuthError::Internal(_) => AuthError::CallbackFailure(Box::new(error)),
other => other,
}
}
pub fn parse_rfc3339(value: &str, field: &str) -> AuthResult<DateTime<Utc>> {
DateTime::parse_from_rfc3339(value)
.map(|dt| dt.with_timezone(&Utc))
.map_err(|_error| AuthError::bad_request(format!("Invalid RFC 3339 timestamp for {field}")))
}
pub fn parse_optional_rfc3339(
value: Option<&str>,
field: &str,
) -> AuthResult<Option<DateTime<Utc>>> {
value.map(|inner| parse_rfc3339(inner, field)).transpose()
}
pub enum AfterHook<S: AuthSchema> {
UserCreated(S::User),
AccountCreated(S::Account),
SessionCreated(S::Session),
SessionUpdated(S::Session),
SessionUpdateMissing(String),
VerificationCreated(S::Verification),
VerificationRecordCreated(VerificationSnapshot),
}
impl<S: AuthSchema> AfterHook<S> {
pub async fn dispatch<B: HookBackend>(
&self,
hook: &dyn DatabaseHooks<S, B>,
ctx: &DatabaseHookContext<'_, B>,
) -> AuthResult<()> {
match self {
Self::UserCreated(user) => hook.after_create_user(user, ctx).await,
Self::AccountCreated(account) => hook.after_create_account(account, ctx).await,
Self::SessionCreated(session) => hook
.after_create_session(session, ctx)
.await
.map_err(callback_error),
Self::SessionUpdated(session) => hook.after_update_session(session, ctx).await,
Self::SessionUpdateMissing(token) => {
hook.after_update_session_missing(token, ctx).await
}
Self::VerificationCreated(verification) => {
hook.after_create_verification(verification, ctx).await
}
Self::VerificationRecordCreated(snapshot) => {
hook.after_create_verification_record(snapshot, ctx).await
}
}
}
}
pub struct AfterHookQueue<S: AuthSchema>(tokio::sync::Mutex<Vec<AfterHook<S>>>);
impl<S: AuthSchema> Default for AfterHookQueue<S> {
fn default() -> Self {
Self::new()
}
}
impl<S: AuthSchema> AfterHookQueue<S> {
#[must_use]
pub const fn new() -> Self {
Self(tokio::sync::Mutex::const_new(Vec::new()))
}
pub async fn push(&self, event: AfterHook<S>) {
self.0.lock().await.push(event);
}
pub async fn run<B: HookBackend>(
self,
hooks: &[Arc<dyn DatabaseHooks<S, B>>],
ctx: &DatabaseHookContext<'_, B>,
) -> AuthResult<()> {
for event in self.0.into_inner() {
for hook in hooks {
event.dispatch(hook.as_ref(), ctx).await?;
}
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum RawFieldValue {
Null,
Bool(bool),
Integer(i64),
Number(f64),
Text(String),
Json(serde_json::Value),
}
impl RawFieldValue {
#[expect(
clippy::as_conversions,
clippy::cast_possible_truncation,
reason = "the range guard above makes the integer coercion exact"
)]
pub fn from_js(value: &JsValue) -> Result<Self, serde_json::Error> {
Ok(match value {
JsValue::Null => Self::Null,
JsValue::Bool(value) => Self::Bool(*value),
JsValue::Number(value)
if value.is_finite()
&& value.fract() == 0.0
&& !(*value == 0.0 && value.is_sign_negative())
&& (-9_223_372_036_854_776_000.0..9_223_372_036_854_776_000.0)
.contains(value) =>
{
Self::Integer(*value as i64)
}
JsValue::Number(value) => Self::Number(*value),
JsValue::String(value) => Self::Text(value.clone()),
JsValue::Array(_) | JsValue::Object(_) => Self::Json(value.to_json_value()?),
})
}
}