Skip to main content

better_auth_api/plugins/
email_verification.rs

1use async_trait::async_trait;
2use chrono::{Duration, Utc};
3use serde::{Deserialize, Serialize};
4use uuid::Uuid;
5use validator::Validate;
6
7use better_auth_core::{AuthContext, AuthPlugin, AuthRoute};
8use better_auth_core::{AuthError, AuthResult};
9use better_auth_core::{AuthRequest, AuthResponse, CreateVerification, HttpMethod, UpdateUser};
10use better_auth_core::{AuthUser, AuthVerification, DatabaseAdapter};
11
12/// Email verification plugin for handling email verification flows
13pub struct EmailVerificationPlugin {
14    config: EmailVerificationConfig,
15}
16
17#[derive(Debug, Clone)]
18pub struct EmailVerificationConfig {
19    pub verification_token_expiry_hours: i64,
20    pub send_email_notifications: bool,
21    pub require_verification_for_signin: bool,
22    pub auto_verify_new_users: bool,
23}
24
25// Request structures for email verification endpoints
26#[derive(Debug, Deserialize, Validate)]
27struct SendVerificationEmailRequest {
28    #[validate(email(message = "Invalid email address"))]
29    email: String,
30    #[serde(rename = "callbackURL")]
31    callback_url: Option<String>,
32}
33
34// Response structures
35#[derive(Debug, Serialize)]
36struct StatusResponse {
37    status: bool,
38}
39
40#[derive(Debug, Serialize)]
41struct VerifyEmailResponse<U: Serialize> {
42    user: U,
43    status: bool,
44}
45
46impl EmailVerificationPlugin {
47    pub fn new() -> Self {
48        Self {
49            config: EmailVerificationConfig::default(),
50        }
51    }
52
53    pub fn with_config(config: EmailVerificationConfig) -> Self {
54        Self { config }
55    }
56
57    pub fn verification_token_expiry_hours(mut self, hours: i64) -> Self {
58        self.config.verification_token_expiry_hours = hours;
59        self
60    }
61
62    pub fn send_email_notifications(mut self, send: bool) -> Self {
63        self.config.send_email_notifications = send;
64        self
65    }
66
67    pub fn require_verification_for_signin(mut self, require: bool) -> Self {
68        self.config.require_verification_for_signin = require;
69        self
70    }
71
72    pub fn auto_verify_new_users(mut self, auto_verify: bool) -> Self {
73        self.config.auto_verify_new_users = auto_verify;
74        self
75    }
76}
77
78impl Default for EmailVerificationPlugin {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl Default for EmailVerificationConfig {
85    fn default() -> Self {
86        Self {
87            verification_token_expiry_hours: 24, // 24 hours default expiry
88            send_email_notifications: true,
89            require_verification_for_signin: false,
90            auto_verify_new_users: false,
91        }
92    }
93}
94
95#[async_trait]
96impl<DB: DatabaseAdapter> AuthPlugin<DB> for EmailVerificationPlugin {
97    fn name(&self) -> &'static str {
98        "email-verification"
99    }
100
101    fn routes(&self) -> Vec<AuthRoute> {
102        vec![
103            AuthRoute::post("/send-verification-email", "send_verification_email"),
104            AuthRoute::get("/verify-email", "verify_email"),
105        ]
106    }
107
108    async fn on_request(
109        &self,
110        req: &AuthRequest,
111        ctx: &AuthContext<DB>,
112    ) -> AuthResult<Option<AuthResponse>> {
113        match (req.method(), req.path()) {
114            (HttpMethod::Post, "/send-verification-email") => {
115                Ok(Some(self.handle_send_verification_email(req, ctx).await?))
116            }
117            (HttpMethod::Get, "/verify-email") => {
118                Ok(Some(self.handle_verify_email(req, ctx).await?))
119            }
120            _ => Ok(None),
121        }
122    }
123
124    async fn on_user_created(&self, user: &DB::User, ctx: &AuthContext<DB>) -> AuthResult<()> {
125        // Send verification email for new users if configured
126        if self.config.send_email_notifications
127            && !user.email_verified()
128            && let Some(email) = user.email()
129        {
130            // Gracefully skip if no email provider is configured
131            if ctx.email_provider.is_some() {
132                if let Err(e) = self
133                    .send_verification_email_internal(email, None, ctx)
134                    .await
135                {
136                    eprintln!(
137                        "[email-verification] Failed to send verification email to {}: {}",
138                        email, e
139                    );
140                }
141            } else {
142                eprintln!(
143                    "[email-verification] No email provider configured, skipping verification email for {}",
144                    email
145                );
146            }
147        }
148        Ok(())
149    }
150}
151
152// Implementation methods outside the trait
153impl EmailVerificationPlugin {
154    async fn handle_send_verification_email<DB: DatabaseAdapter>(
155        &self,
156        req: &AuthRequest,
157        ctx: &AuthContext<DB>,
158    ) -> AuthResult<AuthResponse> {
159        let send_req: SendVerificationEmailRequest =
160            match better_auth_core::validate_request_body(req) {
161                Ok(v) => v,
162                Err(resp) => return Ok(resp),
163            };
164
165        // Check if user exists
166        let user = ctx
167            .database
168            .get_user_by_email(&send_req.email)
169            .await?
170            .ok_or_else(|| AuthError::not_found("No user found with this email address"))?;
171
172        // Check if user is already verified
173        if user.email_verified() {
174            return Err(AuthError::bad_request("Email is already verified"));
175        }
176
177        // Send verification email
178        self.send_verification_email_internal(
179            &send_req.email,
180            send_req.callback_url.as_deref(),
181            ctx,
182        )
183        .await?;
184
185        let response = StatusResponse { status: true };
186        Ok(AuthResponse::json(200, &response)?)
187    }
188
189    async fn handle_verify_email<DB: DatabaseAdapter>(
190        &self,
191        req: &AuthRequest,
192        ctx: &AuthContext<DB>,
193    ) -> AuthResult<AuthResponse> {
194        // Extract token from query parameters
195        let token = req
196            .query
197            .get("token")
198            .ok_or_else(|| AuthError::bad_request("Verification token is required"))?;
199
200        let callback_url = req.query.get("callbackURL");
201
202        // Find verification token
203        let verification = ctx
204            .database
205            .get_verification_by_value(token)
206            .await?
207            .ok_or_else(|| AuthError::bad_request("Invalid or expired verification token"))?;
208
209        // Get user by email (stored in identifier field)
210        let user = ctx
211            .database
212            .get_user_by_email(verification.identifier())
213            .await?
214            .ok_or_else(|| AuthError::not_found("User associated with this token not found"))?;
215
216        // Check if already verified
217        if user.email_verified() {
218            let response = VerifyEmailResponse { user, status: true };
219            return Ok(AuthResponse::json(200, &response)?);
220        }
221
222        // Update user email verification status
223        let update_user = UpdateUser {
224            email: None,
225            name: None,
226            image: None,
227            email_verified: Some(true),
228            username: None,
229            display_username: None,
230            role: None,
231            banned: None,
232            ban_reason: None,
233            ban_expires: None,
234            two_factor_enabled: None,
235            metadata: None,
236        };
237
238        let updated_user = ctx.database.update_user(user.id(), update_user).await?;
239
240        // Delete the used verification token
241        ctx.database.delete_verification(verification.id()).await?;
242
243        // If callback URL is provided, redirect
244        if let Some(callback_url) = callback_url {
245            let redirect_url = format!("{}?verified=true", callback_url);
246            let mut headers = std::collections::HashMap::new();
247            headers.insert("Location".to_string(), redirect_url);
248            return Ok(AuthResponse {
249                status: 302,
250                headers,
251                body: Vec::new(),
252            });
253        }
254
255        let response = VerifyEmailResponse {
256            user: updated_user,
257            status: true,
258        };
259        Ok(AuthResponse::json(200, &response)?)
260    }
261
262    async fn send_verification_email_internal<DB: DatabaseAdapter>(
263        &self,
264        email: &str,
265        callback_url: Option<&str>,
266        ctx: &AuthContext<DB>,
267    ) -> AuthResult<()> {
268        // Generate verification token
269        let verification_token = format!("verify_{}", Uuid::new_v4());
270        let expires_at = Utc::now() + Duration::hours(self.config.verification_token_expiry_hours);
271
272        // Create verification token
273        let create_verification = CreateVerification {
274            identifier: email.to_string(),
275            value: verification_token.clone(),
276            expires_at,
277        };
278
279        ctx.database
280            .create_verification(create_verification)
281            .await?;
282
283        // Send email via the configured provider
284        if self.config.send_email_notifications {
285            let verification_url = if let Some(callback_url) = callback_url {
286                format!("{}?token={}", callback_url, verification_token)
287            } else {
288                format!(
289                    "{}/verify-email?token={}",
290                    ctx.config.base_url, verification_token
291                )
292            };
293
294            let subject = "Verify your email address";
295            let html = format!(
296                "<p>Click the link below to verify your email address:</p>\
297                 <p><a href=\"{url}\">Verify Email</a></p>",
298                url = verification_url
299            );
300            let text = format!("Verify your email address: {}", verification_url);
301
302            ctx.email_provider()?
303                .send(email, subject, &html, &text)
304                .await?;
305        }
306
307        Ok(())
308    }
309
310    /// Check if email verification is required for signin
311    pub fn is_verification_required(&self) -> bool {
312        self.config.require_verification_for_signin
313    }
314
315    /// Check if user is verified or verification is not required
316    pub async fn is_user_verified_or_not_required(&self, user: &impl AuthUser) -> bool {
317        user.email_verified() || !self.config.require_verification_for_signin
318    }
319}