reifydb_auth/
challenge.rs1use std::{
9 collections::HashMap,
10 sync::RwLock,
11 time::{Duration, Instant},
12};
13
14use reifydb_type::value::uuid::Uuid7;
15
16struct ChallengeEntry {
18 pub identifier: String,
19 pub method: String,
20 pub payload: HashMap<String, String>,
21 pub created_at: Instant,
22}
23
24pub struct ChallengeInfo {
26 pub identifier: String,
27 pub method: String,
28 pub payload: HashMap<String, String>,
29}
30
31pub struct ChallengeStore {
37 entries: RwLock<HashMap<String, ChallengeEntry>>,
38 ttl: Duration,
39}
40
41impl ChallengeStore {
42 pub fn new(ttl: Duration) -> Self {
43 Self {
44 entries: RwLock::new(HashMap::new()),
45 ttl,
46 }
47 }
48
49 pub fn create(&self, identifier: String, method: String, payload: HashMap<String, String>) -> String {
51 let challenge_id = Uuid7::generate().to_string();
52 let entry = ChallengeEntry {
53 identifier,
54 method,
55 payload,
56 created_at: Instant::now(),
57 };
58 let mut entries = self.entries.write().unwrap();
59 entries.insert(challenge_id.clone(), entry);
60 challenge_id
61 }
62
63 pub fn consume(&self, challenge_id: &str) -> Option<ChallengeInfo> {
66 let mut entries = self.entries.write().unwrap();
67 let entry = entries.remove(challenge_id)?;
68
69 if entry.created_at.elapsed() > self.ttl {
70 return None;
71 }
72
73 Some(ChallengeInfo {
74 identifier: entry.identifier,
75 method: entry.method,
76 payload: entry.payload,
77 })
78 }
79
80 pub fn cleanup_expired(&self) {
82 let ttl = self.ttl;
83 let mut entries = self.entries.write().unwrap();
84 entries.retain(|_, e| e.created_at.elapsed() <= ttl);
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use std::thread;
91
92 use super::*;
93
94 #[test]
95 fn test_create_and_consume() {
96 let store = ChallengeStore::new(Duration::from_secs(60));
97 let data = HashMap::from([("nonce".to_string(), "abc123".to_string())]);
98
99 let id = store.create("alice".to_string(), "solana".to_string(), data);
100 let info = store.consume(&id).unwrap();
101
102 assert_eq!(info.identifier, "alice");
103 assert_eq!(info.method, "solana");
104 assert_eq!(info.payload.get("nonce").unwrap(), "abc123");
105 }
106
107 #[test]
108 fn test_one_time_use() {
109 let store = ChallengeStore::new(Duration::from_secs(60));
110 let id = store.create("alice".to_string(), "solana".to_string(), HashMap::new());
111
112 assert!(store.consume(&id).is_some());
113 assert!(store.consume(&id).is_none()); }
115
116 #[test]
117 fn test_unknown_challenge() {
118 let store = ChallengeStore::new(Duration::from_secs(60));
119 assert!(store.consume("nonexistent").is_none());
120 }
121
122 #[test]
123 fn test_expired_challenge() {
124 let store = ChallengeStore::new(Duration::from_millis(1));
125 let id = store.create("alice".to_string(), "solana".to_string(), HashMap::new());
126
127 thread::sleep(Duration::from_millis(10));
128 assert!(store.consume(&id).is_none());
129 }
130}