1use crate::api::{ApiResponse, ApiState, extract_bearer_token, validate_api_token};
17use axum::{Json, extract::State, http::HeaderMap};
18use ring::rand::SecureRandom;
19use serde::{Deserialize, Serialize};
20use std::time::Duration;
21
22const VERIFICATION_TOKEN_TTL: Duration = Duration::from_secs(24 * 60 * 60);
24
25const VERIFY_KEY_PREFIX: &str = "email_verify:";
27
28#[derive(Debug, Serialize)]
30pub struct VerificationSentResponse {
31 pub sent: bool,
33 pub verification_token: String,
35 pub message: String,
37}
38
39#[derive(Debug, Deserialize)]
41pub struct VerifyEmailRequest {
42 pub token: String,
44}
45
46#[derive(Debug, Deserialize)]
48pub struct ResendVerificationRequest {
49 pub email: String,
51}
52
53fn generate_verification_token() -> Result<String, crate::errors::AuthError> {
55 let rng = ring::rand::SystemRandom::new();
56 let mut buf = [0u8; 32];
57 rng.fill(&mut buf)
58 .map_err(|_| crate::errors::AuthError::crypto("Failed to generate verification token"))?;
59 Ok(base64::Engine::encode(
60 &base64::engine::general_purpose::URL_SAFE_NO_PAD,
61 buf,
62 ))
63}
64
65pub async fn send_verification(
70 State(state): State<ApiState>,
71 headers: HeaderMap,
72) -> ApiResponse<VerificationSentResponse> {
73 let token = match extract_bearer_token(&headers) {
74 Some(t) => t,
75 None => {
76 return ApiResponse::error_typed("AUTH_REQUIRED", "Bearer token required");
77 }
78 };
79
80 let auth_token = match validate_api_token(&state.auth_framework, &token).await {
81 Ok(t) => t,
82 Err(_) => {
83 return ApiResponse::error_typed("INVALID_TOKEN", "Invalid or expired token");
84 }
85 };
86
87 let user_id = &auth_token.user_id;
88
89 let user_key = format!("user:{user_id}");
91 let user_bytes = match state.auth_framework.storage().get_kv(&user_key).await {
92 Ok(Some(b)) => b,
93 _ => {
94 return ApiResponse::error_typed("USER_NOT_FOUND", "User not found");
95 }
96 };
97 let user_json: serde_json::Value = match serde_json::from_slice(&user_bytes) {
98 Ok(v) => v,
99 Err(_) => {
100 return ApiResponse::error_typed("INTERNAL_ERROR", "Failed to read user record");
101 }
102 };
103
104 if user_json
105 .get("email_verified")
106 .and_then(|v| v.as_bool())
107 .unwrap_or(false)
108 {
109 return ApiResponse::success(VerificationSentResponse {
110 sent: false,
111 verification_token: String::new(),
112 message: "Email is already verified".to_string(),
113 });
114 }
115
116 let verify_token = match generate_verification_token() {
118 Ok(t) => t,
119 Err(_) => {
120 return ApiResponse::error_typed(
121 "INTERNAL_ERROR",
122 "Failed to generate verification token",
123 );
124 }
125 };
126
127 let verify_key = format!("{VERIFY_KEY_PREFIX}{verify_token}");
129 if let Err(_) = state
130 .auth_framework
131 .storage()
132 .store_kv(
133 &verify_key,
134 user_id.as_bytes(),
135 Some(VERIFICATION_TOKEN_TTL),
136 )
137 .await
138 {
139 return ApiResponse::error_typed("INTERNAL_ERROR", "Failed to store verification token");
140 }
141
142 ApiResponse::success(VerificationSentResponse {
143 sent: true,
144 verification_token: verify_token,
145 message: "Verification token generated. Use POST /auth/verify-email to confirm."
146 .to_string(),
147 })
148}
149
150pub async fn verify_email(
156 State(state): State<ApiState>,
157 Json(body): Json<VerifyEmailRequest>,
158) -> ApiResponse<serde_json::Value> {
159 if body.token.is_empty() {
160 return ApiResponse::error_typed("VALIDATION_ERROR", "Verification token is required");
161 }
162
163 let verify_key = format!("{VERIFY_KEY_PREFIX}{}", body.token);
165 let user_id_bytes = match state.auth_framework.storage().get_kv(&verify_key).await {
166 Ok(Some(b)) => b,
167 Ok(None) => {
168 return ApiResponse::error_typed(
169 "INVALID_TOKEN",
170 "Verification token is invalid or expired",
171 );
172 }
173 Err(_) => {
174 return ApiResponse::error_typed("INTERNAL_ERROR", "Failed to look up token");
175 }
176 };
177
178 let user_id = match String::from_utf8(user_id_bytes) {
179 Ok(id) => id,
180 Err(_) => {
181 return ApiResponse::error_typed("INTERNAL_ERROR", "Corrupted verification token");
182 }
183 };
184
185 let user_key = format!("user:{user_id}");
187 let user_bytes = match state.auth_framework.storage().get_kv(&user_key).await {
188 Ok(Some(b)) => b,
189 _ => {
190 return ApiResponse::error_typed("USER_NOT_FOUND", "User not found");
191 }
192 };
193
194 let mut user_json: serde_json::Value = match serde_json::from_slice(&user_bytes) {
195 Ok(v) => v,
196 Err(_) => {
197 return ApiResponse::error_typed("INTERNAL_ERROR", "Failed to read user record");
198 }
199 };
200
201 user_json["email_verified"] = serde_json::Value::Bool(true);
202
203 if let Err(_) = state
204 .auth_framework
205 .storage()
206 .store_kv(&user_key, user_json.to_string().as_bytes(), None)
207 .await
208 {
209 return ApiResponse::error_typed("INTERNAL_ERROR", "Failed to update user record");
210 }
211
212 let _ = state.auth_framework.storage().delete_kv(&verify_key).await;
214
215 ApiResponse::success(serde_json::json!({
216 "verified": true,
217 "user_id": user_id,
218 "message": "Email address verified successfully"
219 }))
220}
221
222pub async fn resend_verification(
228 State(state): State<ApiState>,
229 Json(body): Json<ResendVerificationRequest>,
230) -> ApiResponse<VerificationSentResponse> {
231 if body.email.is_empty() {
232 return ApiResponse::error_typed("VALIDATION_ERROR", "Email address is required");
233 }
234
235 let email_key = format!("user:email:{}", body.email);
237 let user_id_bytes = match state.auth_framework.storage().get_kv(&email_key).await {
238 Ok(Some(b)) => b,
239 Ok(None) | Err(_) => {
241 return ApiResponse::success(VerificationSentResponse {
242 sent: true,
243 verification_token: String::new(),
244 message:
245 "If an account with that email exists, a verification token has been generated."
246 .to_string(),
247 });
248 }
249 };
250
251 let user_id = match String::from_utf8(user_id_bytes) {
252 Ok(id) => id,
253 Err(_) => {
254 return ApiResponse::success(VerificationSentResponse {
255 sent: true,
256 verification_token: String::new(),
257 message:
258 "If an account with that email exists, a verification token has been generated."
259 .to_string(),
260 });
261 }
262 };
263
264 let user_key = format!("user:{user_id}");
266 let user_bytes = match state.auth_framework.storage().get_kv(&user_key).await {
267 Ok(Some(b)) => b,
268 _ => {
269 return ApiResponse::success(VerificationSentResponse {
270 sent: true,
271 verification_token: String::new(),
272 message:
273 "If an account with that email exists, a verification token has been generated."
274 .to_string(),
275 });
276 }
277 };
278 let user_json: serde_json::Value = match serde_json::from_slice(&user_bytes) {
279 Ok(v) => v,
280 Err(_) => {
281 return ApiResponse::success(VerificationSentResponse {
282 sent: true,
283 verification_token: String::new(),
284 message:
285 "If an account with that email exists, a verification token has been generated."
286 .to_string(),
287 });
288 }
289 };
290
291 if user_json
292 .get("email_verified")
293 .and_then(|v| v.as_bool())
294 .unwrap_or(false)
295 {
296 return ApiResponse::success(VerificationSentResponse {
297 sent: false,
298 verification_token: String::new(),
299 message: "Email is already verified".to_string(),
300 });
301 }
302
303 let verify_token = match generate_verification_token() {
305 Ok(t) => t,
306 Err(_) => {
307 return ApiResponse::error_typed(
308 "INTERNAL_ERROR",
309 "Failed to generate verification token",
310 );
311 }
312 };
313
314 let verify_key = format!("{VERIFY_KEY_PREFIX}{verify_token}");
315 if let Err(_) = state
316 .auth_framework
317 .storage()
318 .store_kv(
319 &verify_key,
320 user_id.as_bytes(),
321 Some(VERIFICATION_TOKEN_TTL),
322 )
323 .await
324 {
325 return ApiResponse::error_typed("INTERNAL_ERROR", "Failed to store verification token");
326 }
327
328 ApiResponse::success(VerificationSentResponse {
329 sent: true,
330 verification_token: verify_token,
331 message: "Verification token generated. Use POST /auth/verify-email to confirm."
332 .to_string(),
333 })
334}
335
336#[cfg(test)]
337mod tests {
338 use super::*;
339
340 #[test]
341 fn test_generate_verification_token() {
342 let token1 = generate_verification_token().expect("should generate token");
343 let token2 = generate_verification_token().expect("should generate token");
344 assert_ne!(token1, token2);
346 assert_eq!(token1.len(), 43);
348 assert!(!token1.contains('+'));
350 assert!(!token1.contains('/'));
351 }
352
353 #[test]
354 fn test_verification_token_ttl() {
355 assert_eq!(VERIFICATION_TOKEN_TTL.as_secs(), 86400);
356 }
357}