auth_framework/auth_modular/mfa/
email.rs1use crate::errors::{AuthError, Result};
4use crate::storage::AuthStorage;
5use serde::{Deserialize, Serialize};
6use serde_json::json;
7use std::sync::Arc;
8use std::time::Duration;
9use tracing::{debug, error, info, warn};
10
11#[derive(Debug, Clone, Serialize, Deserialize)]
13pub struct EmailProviderConfig {
14 pub provider: EmailProvider,
16 pub from_email: String,
18 pub from_name: Option<String>,
20 pub provider_config: ProviderConfig,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize)]
26pub enum EmailProvider {
27 SendGrid,
29 AwsSes,
31 Smtp,
33 Development,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub enum ProviderConfig {
40 SendGrid {
42 api_key: String,
43 endpoint: Option<String>,
44 },
45 AwsSes {
47 region: String,
48 access_key_id: String,
49 secret_access_key: String,
50 },
51 Smtp {
53 host: String,
54 port: u16,
55 username: String,
56 password: String,
57 use_tls: bool,
58 },
59 Development,
61}
62
63impl Default for EmailProviderConfig {
64 fn default() -> Self {
65 Self {
66 provider: EmailProvider::Development,
67 from_email: "noreply@example.com".to_string(),
68 from_name: Some("AuthFramework".to_string()),
69 provider_config: ProviderConfig::Development,
70 }
71 }
72}
73
74pub struct EmailManager {
76 storage: Arc<dyn AuthStorage>,
77 email_config: EmailProviderConfig,
78}
79
80impl EmailManager {
81 pub fn new(storage: Arc<dyn AuthStorage>) -> Self {
83 Self {
84 storage,
85 email_config: EmailProviderConfig::default(),
86 }
87 }
88
89 pub fn new_with_config(
91 storage: Arc<dyn AuthStorage>,
92 email_config: EmailProviderConfig,
93 ) -> Self {
94 Self {
95 storage,
96 email_config,
97 }
98 }
99
100 pub async fn register_email(&self, user_id: &str, email: &str) -> Result<()> {
102 debug!("Registering email for user '{}'", user_id);
103
104 if email.is_empty() {
106 return Err(AuthError::validation("Email address cannot be empty"));
107 }
108
109 if !email.contains('@') || !email.contains('.') {
111 return Err(AuthError::validation(
112 "Email address must be in valid format (user@domain.com)",
113 ));
114 }
115
116 let parts: Vec<&str> = email.split('@').collect();
118 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
119 return Err(AuthError::validation("Email address format is invalid"));
120 }
121
122 let domain = parts[1];
123 if !domain.contains('.') || domain.starts_with('.') || domain.ends_with('.') {
124 return Err(AuthError::validation("Email domain format is invalid"));
125 }
126
127 let key = format!("user:{}:email", user_id);
129 self.storage.store_kv(&key, email.as_bytes(), None).await?;
130
131 info!("Email registered for user '{}': {}", user_id, email);
132 Ok(())
133 }
134
135 pub async fn initiate_challenge(&self, user_id: &str) -> Result<String> {
137 debug!("Initiating email challenge for user '{}'", user_id);
138
139 let challenge_id = crate::utils::string::generate_id(Some("email"));
140
141 info!("Email challenge initiated for user '{}'", user_id);
142 Ok(challenge_id)
143 }
144
145 pub async fn generate_code(&self, challenge_id: &str) -> Result<String> {
147 debug!("Generating email code for challenge '{}'", challenge_id);
148
149 let code = format!("{:06}", rand::random::<u32>() % 1000000);
150
151 let email_key = format!("email_challenge:{}:code", challenge_id);
153 self.storage
154 .store_kv(
155 &email_key,
156 code.as_bytes(),
157 Some(Duration::from_secs(300)), )
159 .await?;
160
161 Ok(code)
162 }
163
164 pub async fn verify_code(&self, challenge_id: &str, code: &str) -> Result<bool> {
166 debug!("Verifying email code for challenge '{}'", challenge_id);
167
168 if challenge_id.is_empty() {
170 return Err(AuthError::validation("Challenge ID cannot be empty"));
171 }
172
173 if code.is_empty() {
174 return Err(AuthError::validation("Email code cannot be empty"));
175 }
176
177 let email_key = format!("email_challenge:{}:code", challenge_id);
179 if let Some(stored_code_data) = self.storage.get_kv(&email_key).await? {
180 let stored_code = std::str::from_utf8(&stored_code_data).unwrap_or("");
181
182 let is_valid_format = code.len() == 6 && code.chars().all(|c| c.is_ascii_digit());
184
185 if !is_valid_format {
186 return Ok(false);
187 }
188
189 let is_valid = stored_code == code;
191
192 if is_valid {
193 let _ = self.storage.delete_kv(&email_key).await;
195 }
196
197 Ok(is_valid)
198 } else {
199 Err(AuthError::validation("Invalid or expired challenge ID"))
201 }
202 }
203
204 pub async fn send_code(&self, user_id: &str, code: &str) -> Result<()> {
206 debug!("Sending email code to user '{}'", user_id);
207
208 let email_key = format!("user:{}:email", user_id);
210 if let Some(email_data) = self.storage.get_kv(&email_key).await? {
211 let email_address = String::from_utf8(email_data).map_err(|e| {
212 AuthError::internal(format!("Failed to parse email address: {}", e))
213 })?;
214
215 match self.send_email_via_provider(&email_address, "MFA Code", &format!(
217 "Your authentication code is: {}\n\nThis code will expire in 5 minutes.\nIf you didn't request this code, please ignore this email.",
218 code
219 )).await {
220 Ok(()) => {
221 info!(
222 "Email code '{}' sent successfully to {} for user '{}' via {:?}",
223 code, email_address, user_id, self.email_config.provider
224 );
225 Ok(())
226 }
227 Err(e) => {
228 error!(
229 "Failed to send email code to {} for user '{}': {}",
230 email_address, user_id, e
231 );
232 Err(e)
233 }
234 }
235 } else {
236 Err(AuthError::validation(
237 "No email address registered for user",
238 ))
239 }
240 }
241
242 pub async fn get_user_email(&self, user_id: &str) -> Result<Option<String>> {
244 let email_key = format!("user:{}:email", user_id);
245
246 if let Some(email_data) = self.storage.get_kv(&email_key).await? {
247 Ok(Some(String::from_utf8(email_data).map_err(|e| {
248 AuthError::internal(format!("Failed to parse email address: {}", e))
249 })?))
250 } else {
251 Ok(None)
252 }
253 }
254
255 async fn send_email_via_provider(
257 &self,
258 to_email: &str,
259 subject: &str,
260 body: &str,
261 ) -> Result<()> {
262 match &self.email_config.provider {
263 EmailProvider::SendGrid => self.send_via_sendgrid(to_email, subject, body).await,
264 EmailProvider::AwsSes => self.send_via_aws_ses(to_email, subject, body).await,
265 EmailProvider::Smtp => self.send_via_smtp(to_email, subject, body).await,
266 EmailProvider::Development => {
267 info!("📧 [DEVELOPMENT] Email would be sent:");
269 info!(" To: {}", to_email);
270 info!(" Subject: {}", subject);
271 info!(" Body: {}", body);
272 Ok(())
273 }
274 }
275 }
276
277 async fn send_via_sendgrid(&self, to_email: &str, subject: &str, body: &str) -> Result<()> {
279 if let ProviderConfig::SendGrid { api_key, endpoint } = &self.email_config.provider_config {
280 let client = reqwest::Client::new();
281 let sendgrid_endpoint = endpoint
282 .as_deref()
283 .unwrap_or("https://api.sendgrid.com/v3/mail/send");
284
285 let payload = json!({
286 "personalizations": [{
287 "to": [{"email": to_email}]
288 }],
289 "from": {
290 "email": self.email_config.from_email,
291 "name": self.email_config.from_name.as_deref().unwrap_or("AuthFramework")
292 },
293 "subject": subject,
294 "content": [{
295 "type": "text/plain",
296 "value": body
297 }]
298 });
299
300 let response = client
301 .post(sendgrid_endpoint)
302 .header("Authorization", format!("Bearer {}", api_key))
303 .header("Content-Type", "application/json")
304 .json(&payload)
305 .send()
306 .await
307 .map_err(|e| AuthError::internal(format!("SendGrid request failed: {}", e)))?;
308
309 let status = response.status();
310 if status.is_success() {
311 debug!("SendGrid email sent successfully to {}", to_email);
312 Ok(())
313 } else {
314 let error_text = response.text().await.unwrap_or_default();
315 Err(AuthError::internal(format!(
316 "SendGrid API error: {} - {}",
317 status, error_text
318 )))
319 }
320 } else {
321 Err(AuthError::internal("Invalid SendGrid configuration"))
322 }
323 }
324
325 async fn send_via_aws_ses(&self, to_email: &str, subject: &str, body: &str) -> Result<()> {
327 if let ProviderConfig::AwsSes {
328 region,
329 access_key_id: _,
330 secret_access_key: _,
331 } = &self.email_config.provider_config
332 {
333 warn!("AWS SES integration requires aws-sdk-ses dependency");
336 warn!("Using development fallback for AWS SES");
337
338 info!("📧 [AWS SES DEV] Email would be sent:");
339 info!(" Region: {}", region);
340 info!(" To: {}", to_email);
341 info!(" Subject: {}", subject);
342 info!(" Body: {}", body);
343
344 Ok(())
345 } else {
346 Err(AuthError::internal("Invalid AWS SES configuration"))
347 }
348 }
349
350 async fn send_via_smtp(&self, to_email: &str, subject: &str, body: &str) -> Result<()> {
352 if let ProviderConfig::Smtp {
353 host,
354 port,
355 username: _,
356 password: _,
357 use_tls,
358 } = &self.email_config.provider_config
359 {
360 warn!("SMTP integration requires lettre dependency");
362 warn!("Using development fallback for SMTP");
363
364 info!("📧 [SMTP DEV] Email would be sent:");
365 info!(" Host: {}:{}", host, port);
366 info!(" TLS: {}", use_tls);
367 info!(" To: {}", to_email);
368 info!(" Subject: {}", subject);
369 info!(" Body: {}", body);
370
371 Ok(())
372 } else {
373 Err(AuthError::internal("Invalid SMTP configuration"))
374 }
375 }
376
377 pub async fn has_email(&self, user_id: &str) -> Result<bool> {
379 let email_key = format!("email:{}", user_id);
380 match self.storage.get_kv(&email_key).await {
381 Ok(Some(_)) => Ok(true),
382 Ok(None) => Ok(false),
383 Err(_) => Ok(false), }
385 }
386
387 pub async fn send_email_code(&self, user_id: &str) -> Result<String> {
389 let code = format!("{:06}", rand::random::<u32>() % 1_000_000);
391
392 tracing::info!("Mock email code {} sent to user {}", code, user_id);
394
395 let email_key = format!("email_code:{}", user_id);
397 self.storage
398 .store_kv(
399 &email_key,
400 code.as_bytes(),
401 Some(std::time::Duration::from_secs(300)),
402 )
403 .await?;
404
405 Ok(code)
406 }
407}
408
409