use super::{EmailOtpMessage, EmailOtpPlugin, EmailOtpType};
use crate::plugins::endpoint_context::{EndpointContext, WithCallbacks};
use better_auth_core::background::{BackgroundFuture, run_or_await};
use better_auth_core::{AuthPlugin, AuthResult, AuthSchema, FieldMap};
use std::sync::Arc;
#[cfg(test)]
mod completion_tests;
type Sender<S> = dyn Fn(&EmailOtpMessage, &EndpointContext<'_, S>) -> AuthResult<Option<BackgroundFuture>>
+ Send
+ Sync;
type Generator<S> =
dyn Fn(&str, EmailOtpType, &EndpointContext<'_, S>) -> AuthResult<Option<String>> + Send + Sync;
pub struct EmailOtpCallbacks<S: AuthSchema> {
pub(super) sender: Option<Arc<Sender<S>>>,
pub(super) generator: Option<Arc<Generator<S>>>,
}
impl<S: AuthSchema> Default for EmailOtpCallbacks<S> {
fn default() -> Self {
Self {
sender: None,
generator: None,
}
}
}
impl<S: AuthSchema> EmailOtpCallbacks<S> {
pub fn send<F>(mut self, callback: F) -> Self
where
F: Fn(&EmailOtpMessage, &EndpointContext<'_, S>) -> AuthResult<Option<BackgroundFuture>>
+ Send
+ Sync
+ 'static,
{
self.sender = Some(Arc::new(callback));
self
}
pub fn generate<F>(mut self, callback: F) -> Self
where
F: Fn(&str, EmailOtpType, &EndpointContext<'_, S>) -> AuthResult<Option<String>>
+ Send
+ Sync
+ 'static,
{
self.generator = Some(Arc::new(callback));
self
}
}
impl EmailOtpPlugin {
pub fn callbacks<S: AuthSchema>(self, callbacks: EmailOtpCallbacks<S>) -> impl AuthPlugin<S> {
WithCallbacks {
plugin: self,
callbacks: Arc::new(callbacks),
}
}
}
pub(crate) fn overrides_verification(ctx: &better_auth_core::AuthContext<impl AuthSchema>) -> bool {
ctx.extensions
.get::<super::EmailOtpConfig>()
.is_some_and(|config| config.override_default_email_verification)
}
pub(crate) async fn send_verification_override(
email: &str,
endpoint: &EndpointContext<'_, impl AuthSchema>,
) -> AuthResult<()> {
let ctx = endpoint.auth;
let config = ctx
.extensions
.get::<super::EmailOtpConfig>()
.ok_or_else(|| {
better_auth_core::AuthError::config("Email OTP override is not configured")
})?;
let plugin = EmailOtpPlugin::with_config(config.clone());
let email = email.to_lowercase();
let kind = EmailOtpType::EmailVerification;
let owned = endpoint.to_owned();
let request_context =
better_auth_core::hooks::current_request_hook_context().map(|mut context| {
context.path = Some("/email-otp/send-verification-otp".into());
context.body = better_auth_core::FieldMap::from([
("email".into(), email.clone().into()),
("type".into(), "email-verification".into()),
])
.into();
context.params.clear();
context
});
let task = BackgroundFuture::native(async move {
let operation = async {
let mut endpoint = owned.as_endpoint();
endpoint.body = better_auth_core::FieldMap::from([
("email".into(), email.clone().into()),
("type".into(), "email-verification".into()),
])
.into();
endpoint.path = Some("/email-otp/send-verification-otp");
endpoint.session = None;
endpoint.response = None;
endpoint.params.clear();
let otp = plugin.resolve_otp(&endpoint, &email, kind).await?;
let user = match endpoint.transaction {
Some(transaction) => transaction.get_user_by_email(&email).await?,
None => endpoint.auth.database.get_user_by_email(&email).await?,
};
if user.is_none() {
let identifier = kind.identifier(&email);
match endpoint.transaction {
Some(transaction) => {
transaction
.delete_verification_by_identifier(&identifier)
.await?
}
None => {
endpoint
.auth
.database
.delete_verification_by_identifier(&identifier)
.await?
}
}
} else {
plugin.deliver(&endpoint, &email, otp, kind).await?;
}
Ok(FieldMap::from([("success".into(), true.into())]).into())
};
match request_context {
Some(context) => {
better_auth_core::hooks::with_request_hook_context_value(context, operation).await
}
None => operation.await,
}
});
run_or_await(
Some(task),
ctx.config.advanced.background_tasks.as_ref(),
ctx.logger(),
)
.await?;
Ok(())
}