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
10pub 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#[derive(Debug, Deserialize)]
25struct SendVerificationEmailRequest {
26 email: String,
27 #[serde(rename = "callbackURL")]
28 callback_url: Option<String>,
29}
30
31#[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, 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 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
129impl 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 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 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 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 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 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 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 if user.email_verified {
211 let response = VerifyEmailResponse {
212 user,
213 status: true,
214 };
215 return Ok(AuthResponse::json(200, &response)?);
216 }
217
218 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 ctx.database.delete_verification(&verification.id).await?;
238
239 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 let verification_token = format!("verify_{}", Uuid::new_v4());
259 let expires_at = Utc::now() + Duration::hours(self.config.verification_token_expiry_hours);
260
261 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 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 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 pub fn is_verification_required(&self) -> bool {
309 self.config.require_verification_for_signin
310 }
311
312 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}