swadhinbiswas

[email protected]
2026-07-25 12:46:41 +0600 committed NaN years ago
35f7eb0
commit 35f7eb0
2 files changed
+130 -0
▼crates/justapi-core/src/mail/otp.rs
+28-0
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
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