koprogo_api/domain/entities/
two_factor_secret.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Clone, Serialize, Deserialize, PartialEq)]
8pub struct TwoFactorSecret {
9 pub id: Uuid,
10 pub user_id: Uuid,
11 pub secret_encrypted: String, pub backup_codes_encrypted: Vec<String>, pub is_enabled: bool,
14 pub verified_at: Option<DateTime<Utc>>, pub last_used_at: Option<DateTime<Utc>>, pub created_at: DateTime<Utc>,
17 pub updated_at: DateTime<Utc>,
18}
19
20impl TwoFactorSecret {
21 pub fn new(user_id: Uuid, secret_encrypted: String) -> Result<Self, String> {
23 if secret_encrypted.trim().is_empty() {
25 return Err("Secret cannot be empty".to_string());
26 }
27
28 let backup_codes = Self::generate_backup_codes_placeholders();
30
31 Ok(Self {
32 id: Uuid::new_v4(),
33 user_id,
34 secret_encrypted,
35 backup_codes_encrypted: backup_codes,
36 is_enabled: false,
37 verified_at: None,
38 last_used_at: None,
39 created_at: Utc::now(),
40 updated_at: Utc::now(),
41 })
42 }
43
44 pub fn with_backup_codes(
46 mut self,
47 backup_codes_encrypted: Vec<String>,
48 ) -> Result<Self, String> {
49 if backup_codes_encrypted.len() != 10 {
50 return Err("Must provide exactly 10 backup codes".to_string());
51 }
52
53 self.backup_codes_encrypted = backup_codes_encrypted;
54 Ok(self)
55 }
56
57 pub fn enable(&mut self) -> Result<(), String> {
59 if self.is_enabled {
60 return Err("2FA is already enabled".to_string());
61 }
62
63 self.is_enabled = true;
64 self.verified_at = Some(Utc::now());
65 self.updated_at = Utc::now();
66
67 Ok(())
68 }
69
70 pub fn disable(&mut self) {
72 self.is_enabled = false;
73 self.updated_at = Utc::now();
74 }
75
76 pub fn mark_used(&mut self) {
78 self.last_used_at = Some(Utc::now());
79 self.updated_at = Utc::now();
80 }
81
82 pub fn regenerate_backup_codes(
84 &mut self,
85 new_backup_codes_encrypted: Vec<String>,
86 ) -> Result<(), String> {
87 if new_backup_codes_encrypted.len() != 10 {
88 return Err("Must provide exactly 10 backup codes".to_string());
89 }
90
91 self.backup_codes_encrypted = new_backup_codes_encrypted;
92 self.updated_at = Utc::now();
93
94 Ok(())
95 }
96
97 pub fn remove_backup_code(&mut self, code_index: usize) -> Result<(), String> {
99 if code_index >= self.backup_codes_encrypted.len() {
100 return Err("Invalid backup code index".to_string());
101 }
102
103 self.backup_codes_encrypted.remove(code_index);
104 self.updated_at = Utc::now();
105
106 Ok(())
107 }
108
109 pub fn backup_codes_low(&self) -> bool {
111 self.backup_codes_encrypted.len() < 3
112 }
113
114 pub fn needs_reverification(&self) -> bool {
116 if !self.is_enabled {
117 return false;
118 }
119
120 match self.last_used_at {
121 Some(last_used) => {
122 let days_since_use = (Utc::now() - last_used).num_days();
123 days_since_use > 90
124 }
125 None => false, }
127 }
128
129 fn generate_backup_codes_placeholders() -> Vec<String> {
131 vec!["placeholder".to_string(); 10]
132 }
133}
134
135impl std::fmt::Debug for TwoFactorSecret {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 f.debug_struct("TwoFactorSecret")
140 .field("id", &self.id)
141 .field("user_id", &self.user_id)
142 .field("secret_encrypted", &"[REDACTED]")
143 .field(
144 "backup_codes_encrypted",
145 &format!("[REDACTED × {}]", self.backup_codes_encrypted.len()),
146 )
147 .field("is_enabled", &self.is_enabled)
148 .field("verified_at", &self.verified_at)
149 .field("last_used_at", &self.last_used_at)
150 .field("created_at", &self.created_at)
151 .field("updated_at", &self.updated_at)
152 .finish()
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 fn sample_user_id() -> Uuid {
161 Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap()
162 }
163
164 #[test]
165 fn test_create_two_factor_secret_success() {
166 let secret = TwoFactorSecret::new(
167 sample_user_id(),
168 "JBSWY3DPEHPK3PXP".to_string(), );
170
171 assert!(secret.is_ok());
172 let s = secret.unwrap();
173 assert_eq!(s.user_id, sample_user_id());
174 assert_eq!(s.secret_encrypted, "JBSWY3DPEHPK3PXP");
175 assert!(!s.is_enabled);
176 assert!(s.verified_at.is_none());
177 assert_eq!(s.backup_codes_encrypted.len(), 10);
178 }
179
180 #[test]
181 fn test_create_two_factor_secret_empty() {
182 let secret = TwoFactorSecret::new(sample_user_id(), "".to_string());
183
184 assert!(secret.is_err());
185 assert!(secret.unwrap_err().contains("Secret cannot be empty"));
186 }
187
188 #[test]
189 fn test_with_backup_codes() {
190 let codes: Vec<String> = (0..10).map(|i| format!("CODE{}", i)).collect();
191
192 let secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string())
193 .unwrap()
194 .with_backup_codes(codes.clone());
195
196 assert!(secret.is_ok());
197 let s = secret.unwrap();
198 assert_eq!(s.backup_codes_encrypted.len(), 10);
199 assert_eq!(s.backup_codes_encrypted[0], "CODE0");
200 }
201
202 #[test]
203 fn test_with_backup_codes_invalid_count() {
204 let codes: Vec<String> = vec!["CODE1".to_string(), "CODE2".to_string()]; let secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string())
207 .unwrap()
208 .with_backup_codes(codes);
209
210 assert!(secret.is_err());
211 assert!(secret
212 .unwrap_err()
213 .contains("Must provide exactly 10 backup codes"));
214 }
215
216 #[test]
217 fn test_enable_2fa() {
218 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
219
220 assert!(!secret.is_enabled);
221 assert!(secret.verified_at.is_none());
222
223 let result = secret.enable();
224 assert!(result.is_ok());
225 assert!(secret.is_enabled);
226 assert!(secret.verified_at.is_some());
227 assert!(secret.verified_at.unwrap() <= Utc::now());
228 }
229
230 #[test]
231 fn test_enable_2fa_already_enabled() {
232 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
233
234 secret.enable().unwrap();
235
236 let result = secret.enable();
237 assert!(result.is_err());
238 assert!(result.unwrap_err().contains("already enabled"));
239 }
240
241 #[test]
242 fn test_disable_2fa() {
243 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
244
245 secret.enable().unwrap();
246 assert!(secret.is_enabled);
247
248 secret.disable();
249 assert!(!secret.is_enabled);
250 }
251
252 #[test]
253 fn test_mark_used() {
254 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
255
256 assert!(secret.last_used_at.is_none());
257
258 secret.mark_used();
259 assert!(secret.last_used_at.is_some());
260 assert!(secret.last_used_at.unwrap() <= Utc::now());
261 }
262
263 #[test]
264 fn test_regenerate_backup_codes() {
265 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
266
267 let new_codes: Vec<String> = (0..10).map(|i| format!("NEWCODE{}", i)).collect();
268
269 let result = secret.regenerate_backup_codes(new_codes.clone());
270 assert!(result.is_ok());
271 assert_eq!(secret.backup_codes_encrypted.len(), 10);
272 assert_eq!(secret.backup_codes_encrypted[0], "NEWCODE0");
273 }
274
275 #[test]
276 fn test_regenerate_backup_codes_invalid_count() {
277 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
278
279 let new_codes: Vec<String> = vec!["CODE1".to_string()]; let result = secret.regenerate_backup_codes(new_codes);
282 assert!(result.is_err());
283 }
284
285 #[test]
286 fn test_remove_backup_code() {
287 let codes: Vec<String> = (0..10).map(|i| format!("CODE{}", i)).collect();
288 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string())
289 .unwrap()
290 .with_backup_codes(codes)
291 .unwrap();
292
293 assert_eq!(secret.backup_codes_encrypted.len(), 10);
294
295 let result = secret.remove_backup_code(0);
296 assert!(result.is_ok());
297 assert_eq!(secret.backup_codes_encrypted.len(), 9);
298 assert_eq!(secret.backup_codes_encrypted[0], "CODE1"); }
300
301 #[test]
302 fn test_remove_backup_code_invalid_index() {
303 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
304
305 let result = secret.remove_backup_code(20); assert!(result.is_err());
307 assert!(result.unwrap_err().contains("Invalid backup code index"));
308 }
309
310 #[test]
311 fn test_backup_codes_low() {
312 let codes: Vec<String> = (0..10).map(|i| format!("CODE{}", i)).collect();
313 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string())
314 .unwrap()
315 .with_backup_codes(codes)
316 .unwrap();
317
318 assert!(!secret.backup_codes_low());
319
320 for _ in 0..8 {
322 secret.remove_backup_code(0).unwrap();
323 }
324
325 assert_eq!(secret.backup_codes_encrypted.len(), 2);
326 assert!(secret.backup_codes_low());
327 }
328
329 #[test]
330 fn test_needs_reverification() {
331 let mut secret = TwoFactorSecret::new(sample_user_id(), "SECRET123".to_string()).unwrap();
332
333 assert!(!secret.needs_reverification());
335
336 secret.enable().unwrap();
338 assert!(!secret.needs_reverification()); secret.mark_used();
342 assert!(!secret.needs_reverification());
343
344 secret.last_used_at = Some(Utc::now() - chrono::Duration::days(91));
346 assert!(secret.needs_reverification());
347 }
348}