Skip to main content

better_auth_api/plugins/email_otp/
callbacks.rs

1use super::{EmailOtpMessage, EmailOtpPlugin, EmailOtpType};
2use crate::plugins::endpoint_context::{EndpointContext, WithCallbacks};
3use better_auth_core::background::{BackgroundFuture, run_or_await};
4use better_auth_core::{AuthPlugin, AuthResult, AuthSchema};
5use std::sync::Arc;
6
7type Sender<S> = dyn Fn(&EmailOtpMessage, &EndpointContext<'_, S>) -> AuthResult<Option<BackgroundFuture>>
8    + Send
9    + Sync;
10type Generator<S> =
11    dyn Fn(&str, EmailOtpType, &EndpointContext<'_, S>) -> AuthResult<Option<String>> + Send + Sync;
12
13/// Email OTP callbacks with the complete endpoint context.
14pub struct EmailOtpCallbacks<S: AuthSchema> {
15    pub(super) sender: Option<Arc<Sender<S>>>,
16    pub(super) generator: Option<Arc<Generator<S>>>,
17}
18impl<S: AuthSchema> Default for EmailOtpCallbacks<S> {
19    fn default() -> Self {
20        Self {
21            sender: None,
22            generator: None,
23        }
24    }
25}
26impl<S: AuthSchema> EmailOtpCallbacks<S> {
27    /// Construct delivery work after persisting the OTP. Factory errors propagate immediately.
28    /// Retain the endpoint with `to_owned` when delivery uses its runtime or transaction.
29    pub fn send<F>(mut self, callback: F) -> Self
30    where
31        F: Fn(&EmailOtpMessage, &EndpointContext<'_, S>) -> AuthResult<Option<BackgroundFuture>>
32            + Send
33            + Sync
34            + 'static,
35    {
36        self.sender = Some(Arc::new(callback));
37        self
38    }
39    /// Generate OTPs synchronously; `None` or an empty code selects decimal generation.
40    pub fn generate<F>(mut self, callback: F) -> Self
41    where
42        F: Fn(&str, EmailOtpType, &EndpointContext<'_, S>) -> AuthResult<Option<String>>
43            + Send
44            + Sync
45            + 'static,
46    {
47        self.generator = Some(Arc::new(callback));
48        self
49    }
50}
51impl EmailOtpPlugin {
52    /// Attach schema-aware callbacks after configuring the plugin options.
53    pub fn callbacks<S: AuthSchema>(self, callbacks: EmailOtpCallbacks<S>) -> impl AuthPlugin<S> {
54        WithCallbacks {
55            plugin: self,
56            callbacks: Arc::new(callbacks),
57        }
58    }
59}
60
61/// Whether the active OTP plugin replaces default email verification delivery.
62pub(crate) fn overrides_verification(ctx: &better_auth_core::AuthContext<impl AuthSchema>) -> bool {
63    ctx.extensions
64        .get::<super::EmailOtpConfig>()
65        .is_some_and(|config| config.override_default_email_verification)
66}
67
68pub(crate) async fn send_verification_override(
69    email: &str,
70    endpoint: &EndpointContext<'_, impl AuthSchema>,
71) -> AuthResult<()> {
72    let ctx = endpoint.auth;
73    let config = ctx
74        .extensions
75        .get::<super::EmailOtpConfig>()
76        .ok_or_else(|| {
77            better_auth_core::AuthError::config("Email OTP override is not configured")
78        })?;
79    let plugin = EmailOtpPlugin::with_config(config.clone());
80    let email = email.to_lowercase();
81    let kind = EmailOtpType::EmailVerification;
82    let owned = endpoint.to_owned();
83    let request_context =
84        better_auth_core::hooks::current_request_hook_context().map(|mut context| {
85            context.path = Some("/email-otp/send-verification-otp".into());
86            context.body = better_auth_core::FieldMap::from([
87                ("email".into(), email.clone().into()),
88                ("type".into(), "email-verification".into()),
89            ])
90            .into();
91            context.params.clear();
92            context
93        });
94    let task = Box::pin(async move {
95        let operation = async {
96            let mut endpoint = owned.as_endpoint();
97            endpoint.body = better_auth_core::FieldMap::from([
98                ("email".into(), email.clone().into()),
99                ("type".into(), "email-verification".into()),
100            ])
101            .into();
102            endpoint.path = Some("/email-otp/send-verification-otp");
103            // The upstream override starts from its captured init context, not the caller's resolved session or response.
104            endpoint.session = None;
105            endpoint.response = None;
106            endpoint.params.clear();
107            let otp = plugin.resolve_otp(&endpoint, &email, kind).await?;
108            let user = match endpoint.transaction {
109                Some(transaction) => transaction.get_user_by_email(&email).await?,
110                None => endpoint.auth.database.get_user_by_email(&email).await?,
111            };
112            if user.is_none() {
113                let identifier = kind.identifier(&email);
114                match endpoint.transaction {
115                    Some(transaction) => {
116                        transaction
117                            .delete_verification_by_identifier(&identifier)
118                            .await?
119                    }
120                    None => {
121                        endpoint
122                            .auth
123                            .database
124                            .delete_verification_by_identifier(&identifier)
125                            .await?
126                    }
127                }
128                return Ok(());
129            }
130            plugin.deliver(&endpoint, &email, otp, kind).await
131        };
132        match request_context {
133            Some(context) => {
134                better_auth_core::hooks::with_request_hook_context_value(context, operation).await
135            }
136            None => operation.await,
137        }
138    });
139    // The upstream override invokes the complete OTP endpoint through runInBackgroundOrAwait.
140    run_or_await(
141        Some(task),
142        ctx.config.advanced.background_tasks.as_ref(),
143        &ctx.config.logger,
144    )
145    .await;
146    Ok(())
147}