1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::path::PathBuf;
10use std::sync::Arc;
11use std::time::{Duration, Instant};
12use tokio::sync::RwLock;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct StoredUser {
21 pub id: String,
22 pub username: String,
23 pub password_hash: String,
24 pub display_name: String,
25 pub email: String,
26 pub role: String,
27 pub created_at: DateTime<Utc>,
28 pub updated_at: DateTime<Utc>,
29 pub disabled: bool,
30}
31
32#[derive(Debug, Clone)]
34pub struct SessionRecord {
35 pub session_id: String,
36 pub user_id: String,
37 pub username: String,
38 pub role: String,
39 pub created_at: Instant,
40 pub last_activity: Instant,
41 pub absolute_expiry: Instant,
42}
43
44#[derive(Debug, Clone)]
46pub struct SessionConfig {
47 pub idle_timeout: Duration,
48 pub absolute_timeout: Duration,
49 pub max_parallel_sessions: usize,
50 pub renewal_window: Duration,
51}
52
53impl Default for SessionConfig {
54 fn default() -> Self {
55 Self {
56 idle_timeout: Duration::from_secs(30 * 60), absolute_timeout: Duration::from_secs(24 * 3600), max_parallel_sessions: 5,
59 renewal_window: Duration::from_secs(5 * 60), }
61 }
62}
63
64#[derive(Debug)]
66pub struct UserStore {
67 users: HashMap<String, StoredUser>,
68 sessions: HashMap<String, SessionRecord>,
69 file_path: PathBuf,
70 config: SessionConfig,
71}
72
73pub type SharedUserStore = Arc<RwLock<UserStore>>;
74
75#[derive(Debug, Serialize, Deserialize)]
80struct UsersFile {
81 users: Vec<StoredUser>,
82}
83
84impl UserStore {
89 pub fn new(file_path: PathBuf, config: SessionConfig) -> Self {
91 let users = if file_path.exists() {
92 match std::fs::read_to_string(&file_path) {
93 Ok(contents) => match serde_json::from_str::<UsersFile>(&contents) {
94 Ok(data) => {
95 let mut map = HashMap::new();
96 for user in data.users {
97 map.insert(user.username.clone(), user);
98 }
99 tracing::info!("Loaded {} users from {}", map.len(), file_path.display());
100 map
101 }
102 Err(e) => {
103 tracing::error!("Failed to parse users file: {}", e);
104 HashMap::new()
105 }
106 },
107 Err(e) => {
108 tracing::error!("Failed to read users file: {}", e);
109 HashMap::new()
110 }
111 }
112 } else {
113 HashMap::new()
114 };
115
116 Self {
117 users,
118 sessions: HashMap::new(),
119 file_path,
120 config,
121 }
122 }
123
124 pub fn save(&self) -> Result<(), String> {
126 let data = UsersFile {
127 users: self.users.values().cloned().collect(),
128 };
129 let json =
130 serde_json::to_string_pretty(&data).map_err(|e| format!("Serialize error: {e}"))?;
131
132 if let Some(parent) = self.file_path.parent() {
134 let _ = std::fs::create_dir_all(parent);
135 }
136
137 let tmp_path = self.file_path.with_extension("tmp");
139 std::fs::write(&tmp_path, &json).map_err(|e| format!("Write error: {e}"))?;
140 std::fs::rename(&tmp_path, &self.file_path).map_err(|e| format!("Rename error: {e}"))?;
141
142 Ok(())
143 }
144
145 pub fn is_empty(&self) -> bool {
147 self.users.is_empty()
148 }
149
150 pub fn create_user(
152 &mut self,
153 username: &str,
154 password: &str,
155 display_name: &str,
156 email: &str,
157 role: &str,
158 ) -> Result<StoredUser, String> {
159 if self.users.contains_key(username) {
160 return Err(format!("User '{username}' already exists"));
161 }
162
163 if username.is_empty() || username.len() > 64 {
164 return Err("Username must be 1-64 characters".to_string());
165 }
166
167 if password.len() < 8 {
168 return Err("Password must be at least 8 characters".to_string());
169 }
170
171 let password_hash = hash_password(password)?;
172
173 let now = Utc::now();
174 let user = StoredUser {
175 id: uuid::Uuid::new_v4().to_string(),
176 username: username.to_string(),
177 password_hash,
178 display_name: display_name.to_string(),
179 email: email.to_string(),
180 role: role.to_string(),
181 created_at: now,
182 updated_at: now,
183 disabled: false,
184 };
185
186 self.users.insert(username.to_string(), user.clone());
187 self.save()
188 .map_err(|e| format!("Failed to persist user: {e}"))?;
189
190 Ok(user)
191 }
192
193 pub fn verify_password(&self, username: &str, password: &str) -> Result<StoredUser, String> {
195 let user = self
196 .users
197 .get(username)
198 .ok_or_else(|| "Invalid username or password".to_string())?;
199
200 if user.disabled {
201 return Err("Account is disabled".to_string());
202 }
203
204 if !verify_password(password, &user.password_hash)? {
205 return Err("Invalid username or password".to_string());
206 }
207
208 Ok(user.clone())
209 }
210
211 pub fn create_session(&mut self, user: &StoredUser) -> SessionRecord {
213 let now = Instant::now();
214
215 let user_sessions: Vec<String> = self
217 .sessions
218 .iter()
219 .filter(|(_, s)| s.user_id == user.id)
220 .map(|(id, _)| id.clone())
221 .collect();
222
223 if user_sessions.len() >= self.config.max_parallel_sessions {
225 let mut sessions_with_time: Vec<_> = user_sessions
226 .iter()
227 .filter_map(|id| self.sessions.get(id).map(|s| (id.clone(), s.created_at)))
228 .collect();
229 sessions_with_time.sort_by_key(|(_, t)| *t);
230
231 let to_remove = user_sessions.len() - self.config.max_parallel_sessions + 1;
233 for (id, _) in sessions_with_time.iter().take(to_remove) {
234 self.sessions.remove(id);
235 }
236 }
237
238 let session = SessionRecord {
239 session_id: uuid::Uuid::new_v4().to_string(),
240 user_id: user.id.clone(),
241 username: user.username.clone(),
242 role: user.role.clone(),
243 created_at: now,
244 last_activity: now,
245 absolute_expiry: now + self.config.absolute_timeout,
246 };
247
248 self.sessions
249 .insert(session.session_id.clone(), session.clone());
250 session
251 }
252
253 pub fn validate_session(&mut self, session_id: &str) -> Option<&SessionRecord> {
255 let now = Instant::now();
256 let config = self.config.clone();
257
258 let session = self.sessions.get_mut(session_id)?;
259
260 if now >= session.absolute_expiry {
262 self.sessions.remove(session_id);
263 return None;
264 }
265
266 if now.duration_since(session.last_activity) > config.idle_timeout {
268 self.sessions.remove(session_id);
269 return None;
270 }
271
272 let session = self.sessions.get_mut(session_id)?;
274 session.last_activity = now;
275 Some(session)
276 }
277
278 pub fn needs_renewal(&self, session_id: &str) -> bool {
280 if let Some(session) = self.sessions.get(session_id) {
281 let now = Instant::now();
282 let time_remaining = session
283 .absolute_expiry
284 .checked_duration_since(now)
285 .unwrap_or(Duration::ZERO);
286 time_remaining <= self.config.renewal_window
287 } else {
288 false
289 }
290 }
291
292 pub fn revoke_session(&mut self, session_id: &str) -> bool {
294 self.sessions.remove(session_id).is_some()
295 }
296
297 pub fn revoke_all_user_sessions(&mut self, user_id: &str) -> usize {
299 let to_remove: Vec<String> = self
300 .sessions
301 .iter()
302 .filter(|(_, s)| s.user_id == user_id)
303 .map(|(id, _)| id.clone())
304 .collect();
305 let count = to_remove.len();
306 for id in to_remove {
307 self.sessions.remove(&id);
308 }
309 count
310 }
311
312 pub fn cleanup_expired(&mut self) -> usize {
314 let now = Instant::now();
315 let config = self.config.clone();
316 let before = self.sessions.len();
317 self.sessions.retain(|_, s| {
318 now < s.absolute_expiry && now.duration_since(s.last_activity) <= config.idle_timeout
319 });
320 before - self.sessions.len()
321 }
322
323 pub fn list_users(&self) -> Vec<UserSummary> {
325 self.users
326 .values()
327 .map(|u| UserSummary {
328 id: u.id.clone(),
329 username: u.username.clone(),
330 display_name: u.display_name.clone(),
331 email: u.email.clone(),
332 role: u.role.clone(),
333 disabled: u.disabled,
334 created_at: u.created_at,
335 })
336 .collect()
337 }
338
339 pub fn get_user(&self, username: &str) -> Option<&StoredUser> {
341 self.users.get(username)
342 }
343
344 pub fn get_user_by_id(&self, user_id: &str) -> Option<&StoredUser> {
346 self.users.values().find(|u| u.id == user_id)
347 }
348
349 pub fn update_user(
351 &mut self,
352 user_id: &str,
353 display_name: Option<&str>,
354 email: Option<&str>,
355 role: Option<&str>,
356 disabled: Option<bool>,
357 ) -> Result<StoredUser, String> {
358 let user = self
359 .users
360 .values_mut()
361 .find(|u| u.id == user_id)
362 .ok_or_else(|| "User not found".to_string())?;
363
364 if let Some(name) = display_name {
365 user.display_name = name.to_string();
366 }
367 if let Some(email) = email {
368 user.email = email.to_string();
369 }
370 if let Some(role) = role {
371 user.role = role.to_string();
372 }
373 if let Some(disabled) = disabled {
374 user.disabled = disabled;
375 }
376 user.updated_at = Utc::now();
377
378 let updated = user.clone();
379 self.save().map_err(|e| format!("Failed to persist: {e}"))?;
380
381 Ok(updated)
382 }
383
384 pub fn change_password(&mut self, user_id: &str, new_password: &str) -> Result<(), String> {
386 if new_password.len() < 8 {
387 return Err("Password must be at least 8 characters".to_string());
388 }
389
390 let user = self
391 .users
392 .values_mut()
393 .find(|u| u.id == user_id)
394 .ok_or_else(|| "User not found".to_string())?;
395
396 user.password_hash = hash_password(new_password)?;
397 user.updated_at = Utc::now();
398
399 self.save().map_err(|e| format!("Failed to persist: {e}"))?;
400 Ok(())
401 }
402
403 pub fn delete_user(&mut self, user_id: &str) -> Result<(), String> {
405 let username = self
406 .users
407 .values()
408 .find(|u| u.id == user_id)
409 .map(|u| u.username.clone())
410 .ok_or_else(|| "User not found".to_string())?;
411
412 self.users.remove(&username);
413 self.revoke_all_user_sessions(user_id);
414 self.save().map_err(|e| format!("Failed to persist: {e}"))?;
415
416 Ok(())
417 }
418
419 pub const fn session_config(&self) -> &SessionConfig {
421 &self.config
422 }
423}
424
425#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct UserSummary {
431 pub id: String,
432 pub username: String,
433 pub display_name: String,
434 pub email: String,
435 pub role: String,
436 pub disabled: bool,
437 pub created_at: DateTime<Utc>,
438}
439
440fn hash_password(password: &str) -> Result<String, String> {
445 use argon2::password_hash::{rand_core::OsRng, SaltString};
446 use argon2::{Argon2, PasswordHasher};
447
448 let salt = SaltString::generate(&mut OsRng);
449 let argon2 = Argon2::default(); argon2
452 .hash_password(password.as_bytes(), &salt)
453 .map(|h| h.to_string())
454 .map_err(|e| format!("Password hashing failed: {e}"))
455}
456
457fn verify_password(password: &str, hash: &str) -> Result<bool, String> {
458 use argon2::password_hash::PasswordHash;
459 use argon2::{Argon2, PasswordVerifier};
460
461 let parsed_hash = PasswordHash::new(hash).map_err(|e| format!("Invalid password hash: {e}"))?;
462
463 Ok(Argon2::default()
464 .verify_password(password.as_bytes(), &parsed_hash)
465 .is_ok())
466}
467
468#[cfg(test)]
473mod tests {
474 use super::*;
475
476 fn temp_store() -> UserStore {
477 let dir = tempfile::tempdir().unwrap();
478 let path = dir.path().join("users.json");
479 let _ = dir.keep();
480 UserStore::new(path, SessionConfig::default())
481 }
482
483 #[test]
484 fn test_create_and_verify_user() {
485 let mut store = temp_store();
486 let user = store
487 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
488 .unwrap();
489 assert_eq!(user.username, "alice");
490 assert_eq!(user.role, "admin");
491
492 let verified = store.verify_password("alice", "password123").unwrap();
494 assert_eq!(verified.id, user.id);
495
496 assert!(store.verify_password("alice", "wrong").is_err());
498 }
499
500 #[test]
501 fn test_duplicate_username() {
502 let mut store = temp_store();
503 store
504 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
505 .unwrap();
506 assert!(store
507 .create_user("alice", "other123456", "Alice2", "a2@test.com", "viewer")
508 .is_err());
509 }
510
511 #[test]
512 fn test_short_password_rejected() {
513 let mut store = temp_store();
514 assert!(store
515 .create_user("alice", "short", "Alice", "alice@test.com", "admin")
516 .is_err());
517 }
518
519 #[test]
520 fn test_session_lifecycle() {
521 let mut store = temp_store();
522 let user = store
523 .create_user("bob", "password123", "Bob", "bob@test.com", "operator")
524 .unwrap();
525
526 let session = store.create_session(&user);
527 assert!(!session.session_id.is_empty());
528
529 assert!(store.validate_session(&session.session_id).is_some());
531
532 assert!(store.revoke_session(&session.session_id));
534 assert!(store.validate_session(&session.session_id).is_none());
535 }
536
537 #[test]
538 fn test_max_parallel_sessions() {
539 let config = SessionConfig {
540 max_parallel_sessions: 2,
541 ..Default::default()
542 };
543 let dir = tempfile::tempdir().unwrap();
544 let path = dir.path().join("users.json");
545 let _ = dir.keep();
546 let mut store = UserStore::new(path, config);
547
548 let user = store
549 .create_user("carol", "password123", "Carol", "carol@test.com", "viewer")
550 .unwrap();
551
552 let s1 = store.create_session(&user);
553 let s2 = store.create_session(&user);
554 let s3 = store.create_session(&user); assert!(store.validate_session(&s1.session_id).is_none());
558 assert!(store.validate_session(&s2.session_id).is_some());
559 assert!(store.validate_session(&s3.session_id).is_some());
560 }
561
562 #[test]
563 fn test_list_users() {
564 let mut store = temp_store();
565 store
566 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
567 .unwrap();
568 store
569 .create_user("bob", "password456", "Bob", "bob@test.com", "viewer")
570 .unwrap();
571
572 let list = store.list_users();
573 assert_eq!(list.len(), 2);
574 }
575
576 #[test]
577 fn test_persistence_roundtrip() {
578 let dir = tempfile::tempdir().unwrap();
579 let path = dir.path().join("users.json");
580
581 {
583 let mut store = UserStore::new(path.clone(), SessionConfig::default());
584 store
585 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
586 .unwrap();
587 }
588
589 let store = UserStore::new(path, SessionConfig::default());
591 assert_eq!(store.list_users().len(), 1);
592 assert!(store.verify_password("alice", "password123").is_ok());
593 }
594
595 #[test]
596 fn test_update_user() {
597 let mut store = temp_store();
598 let user = store
599 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
600 .unwrap();
601
602 let updated = store
603 .update_user(&user.id, Some("Alice Updated"), None, Some("viewer"), None)
604 .unwrap();
605
606 assert_eq!(updated.display_name, "Alice Updated");
607 assert_eq!(updated.role, "viewer");
608 }
609
610 #[test]
611 fn test_delete_user() {
612 let mut store = temp_store();
613 let user = store
614 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
615 .unwrap();
616
617 store.delete_user(&user.id).unwrap();
618 assert!(store.list_users().is_empty());
619 }
620
621 #[test]
622 fn test_disabled_user_cannot_login() {
623 let mut store = temp_store();
624 let user = store
625 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
626 .unwrap();
627 store
628 .update_user(&user.id, None, None, None, Some(true))
629 .unwrap();
630
631 assert!(store.verify_password("alice", "password123").is_err());
632 }
633
634 #[test]
635 fn test_change_password() {
636 let mut store = temp_store();
637 let user = store
638 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
639 .unwrap();
640
641 store.change_password(&user.id, "newpassword456").unwrap();
642 assert!(store.verify_password("alice", "password123").is_err());
643 assert!(store.verify_password("alice", "newpassword456").is_ok());
644 }
645
646 #[test]
647 fn test_revoke_all_user_sessions() {
648 let mut store = temp_store();
649 let user = store
650 .create_user("alice", "password123", "Alice", "alice@test.com", "admin")
651 .unwrap();
652
653 store.create_session(&user);
654 store.create_session(&user);
655 store.create_session(&user);
656
657 let revoked = store.revoke_all_user_sessions(&user.id);
658 assert_eq!(revoked, 3);
659 }
660}