better_auth/plugins/
email_verification.rs

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