35f7eb0
commit 35f7eb0
2 files changed
+130 -0
Showing 2 changed files+130-0
▼crates/justapi-core/src/mail/otp.rs
+28-0
| @@ -0,0 +1,28 @@ | ||
1 | +use std::time::{SystemTime, UNIX_EPOCH}; | |
2 | + | |
3 | +use super::verify::{VerificationEntry, VerificationStore}; | |
4 | + | |
5 | +impl VerificationStore { | |
6 | + /// Generate a numeric OTP (6 digits) and store it. | |
7 | + pub async fn create_otp( | |
8 | + &self, | |
9 | + email: &str, | |
10 | + purpose: &str, | |
11 | + _ttl_secs: u64, | |
12 | + ) -> String { | |
13 | + use rand::Rng; | |
14 | + let otp: String = format!("{:06}", rand::thread_rng().gen_range(0..1_000_000)); | |
15 | + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); | |
16 | + let mut store = self.tokens.write().await; | |
17 | + store.insert( | |
18 | + otp.clone(), | |
19 | + VerificationEntry { | |
20 | + email: email.to_string(), | |
21 | + purpose: purpose.to_string(), | |
22 | + created_at: now, | |
23 | + used: false, | |
24 | + }, | |
25 | + ); | |
26 | + otp | |
27 | + } | |
28 | +} | |
0 | 29 | |
▼crates/justapi-core/src/mail/verify.rs
+102-0
| @@ -0,0 +1,102 @@ | ||
1 | +use std::collections::HashMap; | |
2 | +use std::sync::Arc; | |
3 | +use std::time::{SystemTime, UNIX_EPOCH}; | |
4 | + | |
5 | +use tokio::sync::RwLock; | |
6 | + | |
7 | +/// In-memory store for email verification tokens. | |
8 | +/// In production, use Redis or the database instead. | |
9 | +pub struct VerificationStore { | |
10 | + pub(crate) tokens: Arc<RwLock<HashMap<String, VerificationEntry>>>, | |
11 | +} | |
12 | + | |
13 | +#[derive(Clone)] | |
14 | +pub(crate) struct VerificationEntry { | |
15 | + pub email: String, | |
16 | + pub purpose: String, | |
17 | + pub created_at: u64, | |
18 | + pub used: bool, | |
19 | +} | |
20 | + | |
21 | +impl VerificationStore { | |
22 | + pub fn new() -> Self { | |
23 | + Self { tokens: Arc::new(RwLock::new(HashMap::new())) } | |
24 | + } | |
25 | + | |
26 | + /// Generate a new verification token and store it. | |
27 | + pub async fn create_token( | |
28 | + &self, | |
29 | + email: &str, | |
30 | + purpose: &str, | |
31 | + _ttl_secs: u64, | |
32 | + ) -> String { | |
33 | + use rand::Rng; | |
34 | + let token: String = rand::thread_rng() | |
35 | + .sample_iter(&rand::distributions::Alphanumeric) | |
36 | + .take(32) | |
37 | + .map(char::from) | |
38 | + .collect(); | |
39 | + | |
40 | + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); | |
41 | + | |
42 | + let mut store = self.tokens.write().await; | |
43 | + store.insert( | |
44 | + token.clone(), | |
45 | + VerificationEntry { | |
46 | + email: email.to_string(), | |
47 | + purpose: purpose.to_string(), | |
48 | + created_at: now, | |
49 | + used: false, | |
50 | + }, | |
51 | + ); | |
52 | + | |
53 | + token | |
54 | + } | |
55 | + | |
56 | + /// Verify a token: returns the email if valid, None otherwise. | |
57 | + /// Consumes the token (one-time use). | |
58 | + pub async fn verify( | |
59 | + &self, | |
60 | + token: &str, | |
61 | + purpose: &str, | |
62 | + max_age_secs: u64, | |
63 | + ) -> Option<String> { | |
64 | + let mut store = self.tokens.write().await; | |
65 | + let mut entry = match store.get(token) { | |
66 | + Some(e) => e.clone(), | |
67 | + None => return None, | |
68 | + }; | |
69 | + | |
70 | + if entry.used { | |
71 | + return None; | |
72 | + } | |
73 | + if entry.purpose != purpose { | |
74 | + return None; | |
75 | + } | |
76 | + | |
77 | + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); | |
78 | + if now - entry.created_at > max_age_secs { | |
79 | + store.remove(token); | |
80 | + return None; | |
81 | + } | |
82 | + | |
83 | + let email = entry.email.clone(); | |
84 | + entry.used = true; | |
85 | + store.insert(token.to_string(), entry); | |
86 | + | |
87 | + Some(email) | |
88 | + } | |
89 | + | |
90 | + /// Clean up expired tokens from the store. | |
91 | + pub async fn cleanup(&self, max_age_secs: u64) { | |
92 | + let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs(); | |
93 | + let mut store = self.tokens.write().await; | |
94 | + store.retain(|_, entry| now - entry.created_at <= max_age_secs); | |
95 | + } | |
96 | +} | |
97 | + | |
98 | +impl Default for VerificationStore { | |
99 | + fn default() -> Self { | |
100 | + Self::new() | |
101 | + } | |
102 | +} | |
0 | 103 | |