1use std::path::Path;
4
5use argon2::{
6 Argon2,
7 password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
8};
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use super::AuthError;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum UserRole {
18 Admin,
20 Operator,
22 Viewer,
24}
25
26impl UserRole {
27 #[must_use]
29 pub const fn is_admin(&self) -> bool {
30 matches!(self, Self::Admin)
31 }
32
33 #[must_use]
35 pub const fn can_manage_sessions(&self) -> bool {
36 matches!(self, Self::Admin | Self::Operator)
37 }
38
39 #[must_use]
41 pub const fn can_view(&self) -> bool {
42 true }
44}
45
46impl std::fmt::Display for UserRole {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Admin => write!(f, "admin"),
50 Self::Operator => write!(f, "operator"),
51 Self::Viewer => write!(f, "viewer"),
52 }
53 }
54}
55
56impl std::str::FromStr for UserRole {
57 type Err = AuthError;
58
59 fn from_str(s: &str) -> Result<Self, Self::Err> {
60 match s.to_lowercase().as_str() {
61 "admin" => Ok(Self::Admin),
62 "operator" => Ok(Self::Operator),
63 "viewer" => Ok(Self::Viewer),
64 _ => Err(AuthError::Config(format!("Unknown role: {s}"))),
65 }
66 }
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct User {
72 pub id: String,
74 pub username: String,
76 pub email: Option<String>,
78 pub password_hash: String,
80 pub role: UserRole,
82 pub created_at: DateTime<Utc>,
84 pub last_login: Option<DateTime<Utc>>,
86 pub active: bool,
88}
89
90impl User {
91 pub fn new(
97 username: impl Into<String>,
98 password: &str,
99 role: UserRole,
100 ) -> Result<Self, AuthError> {
101 let username = username.into();
102 let id = format!("user_{}", uuid_v4());
103 let password_hash = hash_password(password)?;
104
105 Ok(Self {
106 id,
107 username,
108 email: None,
109 password_hash,
110 role,
111 created_at: Utc::now(),
112 last_login: None,
113 active: true,
114 })
115 }
116
117 pub fn verify_password(&self, password: &str) -> Result<(), AuthError> {
123 verify_password(password, &self.password_hash)
124 }
125
126 pub fn set_password(&mut self, password: &str) -> Result<(), AuthError> {
132 self.password_hash = hash_password(password)?;
133 Ok(())
134 }
135
136 #[must_use]
138 pub fn to_public(&self) -> PublicUser {
139 PublicUser {
140 id: self.id.clone(),
141 username: self.username.clone(),
142 email: self.email.clone(),
143 role: self.role,
144 created_at: self.created_at,
145 last_login: self.last_login,
146 active: self.active,
147 }
148 }
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct PublicUser {
154 pub id: String,
156 pub username: String,
158 pub email: Option<String>,
160 pub role: UserRole,
162 pub created_at: DateTime<Utc>,
164 pub last_login: Option<DateTime<Utc>>,
166 pub active: bool,
168}
169
170pub struct UserStore {
172 db: sled::Db,
173 tree: sled::Tree,
174}
175
176impl UserStore {
177 pub fn open(path: &Path) -> Result<Self, AuthError> {
183 let db = sled::open(path.join("auth"))
184 .map_err(|e| AuthError::Storage(format!("Failed to open auth database: {e}")))?;
185
186 let tree = db
187 .open_tree("users")
188 .map_err(|e| AuthError::Storage(format!("Failed to open users tree: {e}")))?;
189
190 Ok(Self { db, tree })
191 }
192
193 pub fn with_db(db: sled::Db) -> Result<Self, AuthError> {
199 let tree = db
200 .open_tree("users")
201 .map_err(|e| AuthError::Storage(format!("Failed to open users tree: {e}")))?;
202
203 Ok(Self { db, tree })
204 }
205
206 #[must_use]
208 pub const fn db(&self) -> &sled::Db {
209 &self.db
210 }
211
212 #[must_use]
214 pub fn is_empty(&self) -> bool {
215 self.tree.is_empty()
216 }
217
218 #[must_use]
220 pub fn count(&self) -> usize {
221 self.tree
223 .iter()
224 .filter(|r| {
225 r.as_ref()
226 .map(|(k, _)| !k.starts_with(b"idx:"))
227 .unwrap_or(false)
228 })
229 .count()
230 }
231
232 pub fn create(&self, user: &User) -> Result<(), AuthError> {
238 if self.get_by_username(&user.username)?.is_some() {
240 return Err(AuthError::UserExists(user.username.clone()));
241 }
242
243 let key = user.id.as_bytes();
244 let value = serde_json::to_vec(user)
245 .map_err(|e| AuthError::Storage(format!("Serialization error: {e}")))?;
246
247 self.tree
248 .insert(key, value)
249 .map_err(|e| AuthError::Storage(format!("Insert error: {e}")))?;
250
251 let index_key = format!("idx:username:{}", user.username);
253 self.tree
254 .insert(index_key.as_bytes(), user.id.as_bytes())
255 .map_err(|e| AuthError::Storage(format!("Index error: {e}")))?;
256
257 self.tree
258 .flush()
259 .map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
260
261 Ok(())
262 }
263
264 pub fn get(&self, id: &str) -> Result<Option<User>, AuthError> {
270 let key = id.as_bytes();
271 match self.tree.get(key) {
272 Ok(Some(value)) => {
273 let user: User = serde_json::from_slice(&value)
274 .map_err(|e| AuthError::Storage(format!("Deserialization error: {e}")))?;
275 Ok(Some(user))
276 }
277 Ok(None) => Ok(None),
278 Err(e) => Err(AuthError::Storage(format!("Get error: {e}"))),
279 }
280 }
281
282 pub fn get_by_username(&self, username: &str) -> Result<Option<User>, AuthError> {
288 let index_key = format!("idx:username:{username}");
289 match self.tree.get(index_key.as_bytes()) {
290 Ok(Some(id_bytes)) => {
291 let id = String::from_utf8_lossy(&id_bytes);
292 self.get(&id)
293 }
294 Ok(None) => Ok(None),
295 Err(e) => Err(AuthError::Storage(format!("Index lookup error: {e}"))),
296 }
297 }
298
299 pub fn update(&self, user: &User) -> Result<(), AuthError> {
305 if self.get(&user.id)?.is_none() {
307 return Err(AuthError::UserNotFound(user.id.clone()));
308 }
309
310 let key = user.id.as_bytes();
311 let value = serde_json::to_vec(user)
312 .map_err(|e| AuthError::Storage(format!("Serialization error: {e}")))?;
313
314 self.tree
315 .insert(key, value)
316 .map_err(|e| AuthError::Storage(format!("Update error: {e}")))?;
317
318 self.tree
319 .flush()
320 .map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
321
322 Ok(())
323 }
324
325 pub fn delete(&self, id: &str) -> Result<bool, AuthError> {
331 if let Some(user) = self.get(id)? {
333 let index_key = format!("idx:username:{}", user.username);
334 self.tree
335 .remove(index_key.as_bytes())
336 .map_err(|e| AuthError::Storage(format!("Index remove error: {e}")))?;
337 }
338
339 let removed = self
340 .tree
341 .remove(id.as_bytes())
342 .map_err(|e| AuthError::Storage(format!("Delete error: {e}")))?
343 .is_some();
344
345 self.tree
346 .flush()
347 .map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
348
349 Ok(removed)
350 }
351
352 pub fn list(&self) -> Result<Vec<User>, AuthError> {
358 let mut users = Vec::new();
359
360 for result in &self.tree {
361 let (key, value) =
362 result.map_err(|e| AuthError::Storage(format!("Iter error: {e}")))?;
363
364 if key.starts_with(b"idx:") {
366 continue;
367 }
368
369 let user: User = serde_json::from_slice(&value)
370 .map_err(|e| AuthError::Storage(format!("Deserialization error: {e}")))?;
371 users.push(user);
372 }
373
374 Ok(users)
375 }
376
377 pub fn update_last_login(&self, id: &str) -> Result<(), AuthError> {
383 let mut user = self
384 .get(id)?
385 .ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
386
387 user.last_login = Some(Utc::now());
388 self.update(&user)
389 }
390}
391
392fn hash_password(password: &str) -> Result<String, AuthError> {
394 let salt = SaltString::generate(&mut OsRng);
395 let argon2 = Argon2::default();
396
397 argon2
398 .hash_password(password.as_bytes(), &salt)
399 .map(|h| h.to_string())
400 .map_err(|e| AuthError::Config(format!("Password hashing failed: {e}")))
401}
402
403fn verify_password(password: &str, hash: &str) -> Result<(), AuthError> {
405 let parsed_hash =
406 PasswordHash::new(hash).map_err(|e| AuthError::Config(format!("Invalid hash: {e}")))?;
407
408 Argon2::default()
409 .verify_password(password.as_bytes(), &parsed_hash)
410 .map_err(|_| AuthError::InvalidCredentials)
411}
412
413fn uuid_v4() -> String {
415 use rand::RngCore;
416 let mut rng = rand::thread_rng();
417 let mut bytes = [0u8; 16];
418 rng.fill_bytes(&mut bytes);
419
420 bytes[6] = (bytes[6] & 0x0f) | 0x40;
422 bytes[8] = (bytes[8] & 0x3f) | 0x80;
423
424 format!(
425 "{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
426 bytes[0],
427 bytes[1],
428 bytes[2],
429 bytes[3],
430 bytes[4],
431 bytes[5],
432 bytes[6],
433 bytes[7],
434 bytes[8],
435 bytes[9],
436 bytes[10],
437 bytes[11],
438 bytes[12],
439 bytes[13],
440 bytes[14],
441 bytes[15]
442 )
443}
444
445#[cfg(test)]
446mod tests {
447 use super::*;
448 use tempfile::TempDir;
449
450 #[test]
451 fn test_user_creation() {
452 let user = User::new("testuser", "password123", UserRole::Admin).unwrap();
453 assert_eq!(user.username, "testuser");
454 assert!(user.id.starts_with("user_"));
455 assert!(user.active);
456 assert_eq!(user.role, UserRole::Admin);
457 }
458
459 #[test]
460 fn test_password_verification() {
461 let user = User::new("testuser", "password123", UserRole::Admin).unwrap();
462 assert!(user.verify_password("password123").is_ok());
463 assert!(user.verify_password("wrongpassword").is_err());
464 }
465
466 #[test]
467 fn test_user_store() {
468 let temp_dir = TempDir::new().unwrap();
469 let store = UserStore::open(temp_dir.path()).unwrap();
470
471 assert!(store.is_empty());
472
473 let user = User::new("admin", "secret", UserRole::Admin).unwrap();
474 store.create(&user).unwrap();
475
476 assert!(!store.is_empty());
477 assert_eq!(store.count(), 1);
478
479 let loaded = store.get(&user.id).unwrap().unwrap();
480 assert_eq!(loaded.username, "admin");
481
482 let by_name = store.get_by_username("admin").unwrap().unwrap();
483 assert_eq!(by_name.id, user.id);
484 }
485
486 #[test]
487 fn test_user_roles() {
488 assert!(UserRole::Admin.is_admin());
489 assert!(!UserRole::Operator.is_admin());
490 assert!(!UserRole::Viewer.is_admin());
491
492 assert!(UserRole::Admin.can_manage_sessions());
493 assert!(UserRole::Operator.can_manage_sessions());
494 assert!(!UserRole::Viewer.can_manage_sessions());
495 }
496
497 #[test]
498 fn test_duplicate_user() {
499 let temp_dir = TempDir::new().unwrap();
500 let store = UserStore::open(temp_dir.path()).unwrap();
501
502 let user1 = User::new("admin", "secret1", UserRole::Admin).unwrap();
503 store.create(&user1).unwrap();
504
505 let user2 = User::new("admin", "secret2", UserRole::Operator).unwrap();
506 let result = store.create(&user2);
507
508 assert!(matches!(result, Err(AuthError::UserExists(_))));
509 }
510}