use std::path::Path;
use argon2::{
Argon2,
password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::AuthError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum UserRole {
Admin,
Operator,
Viewer,
}
impl UserRole {
#[must_use]
pub const fn is_admin(&self) -> bool {
matches!(self, Self::Admin)
}
#[must_use]
pub const fn can_manage_sessions(&self) -> bool {
matches!(self, Self::Admin | Self::Operator)
}
#[must_use]
pub const fn can_view(&self) -> bool {
true }
}
impl std::fmt::Display for UserRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Admin => write!(f, "admin"),
Self::Operator => write!(f, "operator"),
Self::Viewer => write!(f, "viewer"),
}
}
}
impl std::str::FromStr for UserRole {
type Err = AuthError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"admin" => Ok(Self::Admin),
"operator" => Ok(Self::Operator),
"viewer" => Ok(Self::Viewer),
_ => Err(AuthError::Config(format!("Unknown role: {s}"))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: String,
pub username: String,
pub email: Option<String>,
pub password_hash: String,
pub role: UserRole,
pub created_at: DateTime<Utc>,
pub last_login: Option<DateTime<Utc>>,
pub active: bool,
}
impl User {
pub fn new(
username: impl Into<String>,
password: &str,
role: UserRole,
) -> Result<Self, AuthError> {
let username = username.into();
let id = format!("user_{}", uuid_v4());
let password_hash = hash_password(password)?;
Ok(Self {
id,
username,
email: None,
password_hash,
role,
created_at: Utc::now(),
last_login: None,
active: true,
})
}
pub fn verify_password(&self, password: &str) -> Result<(), AuthError> {
verify_password(password, &self.password_hash)
}
pub fn set_password(&mut self, password: &str) -> Result<(), AuthError> {
self.password_hash = hash_password(password)?;
Ok(())
}
#[must_use]
pub fn to_public(&self) -> PublicUser {
PublicUser {
id: self.id.clone(),
username: self.username.clone(),
email: self.email.clone(),
role: self.role,
created_at: self.created_at,
last_login: self.last_login,
active: self.active,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PublicUser {
pub id: String,
pub username: String,
pub email: Option<String>,
pub role: UserRole,
pub created_at: DateTime<Utc>,
pub last_login: Option<DateTime<Utc>>,
pub active: bool,
}
pub struct UserStore {
db: sled::Db,
tree: sled::Tree,
}
impl UserStore {
pub fn open(path: &Path) -> Result<Self, AuthError> {
let db = sled::open(path.join("auth"))
.map_err(|e| AuthError::Storage(format!("Failed to open auth database: {e}")))?;
let tree = db
.open_tree("users")
.map_err(|e| AuthError::Storage(format!("Failed to open users tree: {e}")))?;
Ok(Self { db, tree })
}
pub fn with_db(db: sled::Db) -> Result<Self, AuthError> {
let tree = db
.open_tree("users")
.map_err(|e| AuthError::Storage(format!("Failed to open users tree: {e}")))?;
Ok(Self { db, tree })
}
#[must_use]
pub const fn db(&self) -> &sled::Db {
&self.db
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.tree.is_empty()
}
#[must_use]
pub fn count(&self) -> usize {
self.tree
.iter()
.filter(|r| {
r.as_ref()
.map(|(k, _)| !k.starts_with(b"idx:"))
.unwrap_or(false)
})
.count()
}
pub fn create(&self, user: &User) -> Result<(), AuthError> {
if self.get_by_username(&user.username)?.is_some() {
return Err(AuthError::UserExists(user.username.clone()));
}
let key = user.id.as_bytes();
let value = serde_json::to_vec(user)
.map_err(|e| AuthError::Storage(format!("Serialization error: {e}")))?;
self.tree
.insert(key, value)
.map_err(|e| AuthError::Storage(format!("Insert error: {e}")))?;
let index_key = format!("idx:username:{}", user.username);
self.tree
.insert(index_key.as_bytes(), user.id.as_bytes())
.map_err(|e| AuthError::Storage(format!("Index error: {e}")))?;
self.tree
.flush()
.map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
Ok(())
}
pub fn get(&self, id: &str) -> Result<Option<User>, AuthError> {
let key = id.as_bytes();
match self.tree.get(key) {
Ok(Some(value)) => {
let user: User = serde_json::from_slice(&value)
.map_err(|e| AuthError::Storage(format!("Deserialization error: {e}")))?;
Ok(Some(user))
}
Ok(None) => Ok(None),
Err(e) => Err(AuthError::Storage(format!("Get error: {e}"))),
}
}
pub fn get_by_username(&self, username: &str) -> Result<Option<User>, AuthError> {
let index_key = format!("idx:username:{username}");
match self.tree.get(index_key.as_bytes()) {
Ok(Some(id_bytes)) => {
let id = String::from_utf8_lossy(&id_bytes);
self.get(&id)
}
Ok(None) => Ok(None),
Err(e) => Err(AuthError::Storage(format!("Index lookup error: {e}"))),
}
}
pub fn update(&self, user: &User) -> Result<(), AuthError> {
if self.get(&user.id)?.is_none() {
return Err(AuthError::UserNotFound(user.id.clone()));
}
let key = user.id.as_bytes();
let value = serde_json::to_vec(user)
.map_err(|e| AuthError::Storage(format!("Serialization error: {e}")))?;
self.tree
.insert(key, value)
.map_err(|e| AuthError::Storage(format!("Update error: {e}")))?;
self.tree
.flush()
.map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
Ok(())
}
pub fn delete(&self, id: &str) -> Result<bool, AuthError> {
if let Some(user) = self.get(id)? {
let index_key = format!("idx:username:{}", user.username);
self.tree
.remove(index_key.as_bytes())
.map_err(|e| AuthError::Storage(format!("Index remove error: {e}")))?;
}
let removed = self
.tree
.remove(id.as_bytes())
.map_err(|e| AuthError::Storage(format!("Delete error: {e}")))?
.is_some();
self.tree
.flush()
.map_err(|e| AuthError::Storage(format!("Flush error: {e}")))?;
Ok(removed)
}
pub fn list(&self) -> Result<Vec<User>, AuthError> {
let mut users = Vec::new();
for result in &self.tree {
let (key, value) =
result.map_err(|e| AuthError::Storage(format!("Iter error: {e}")))?;
if key.starts_with(b"idx:") {
continue;
}
let user: User = serde_json::from_slice(&value)
.map_err(|e| AuthError::Storage(format!("Deserialization error: {e}")))?;
users.push(user);
}
Ok(users)
}
pub fn update_last_login(&self, id: &str) -> Result<(), AuthError> {
let mut user = self
.get(id)?
.ok_or_else(|| AuthError::UserNotFound(id.to_string()))?;
user.last_login = Some(Utc::now());
self.update(&user)
}
}
fn hash_password(password: &str) -> Result<String, AuthError> {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password.as_bytes(), &salt)
.map(|h| h.to_string())
.map_err(|e| AuthError::Config(format!("Password hashing failed: {e}")))
}
fn verify_password(password: &str, hash: &str) -> Result<(), AuthError> {
let parsed_hash =
PasswordHash::new(hash).map_err(|e| AuthError::Config(format!("Invalid hash: {e}")))?;
Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.map_err(|_| AuthError::InvalidCredentials)
}
fn uuid_v4() -> String {
use rand::RngCore;
let mut rng = rand::thread_rng();
let mut bytes = [0u8; 16];
rng.fill_bytes(&mut bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0],
bytes[1],
bytes[2],
bytes[3],
bytes[4],
bytes[5],
bytes[6],
bytes[7],
bytes[8],
bytes[9],
bytes[10],
bytes[11],
bytes[12],
bytes[13],
bytes[14],
bytes[15]
)
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::TempDir;
#[test]
fn test_user_creation() {
let user = User::new("testuser", "password123", UserRole::Admin).unwrap();
assert_eq!(user.username, "testuser");
assert!(user.id.starts_with("user_"));
assert!(user.active);
assert_eq!(user.role, UserRole::Admin);
}
#[test]
fn test_password_verification() {
let user = User::new("testuser", "password123", UserRole::Admin).unwrap();
assert!(user.verify_password("password123").is_ok());
assert!(user.verify_password("wrongpassword").is_err());
}
#[test]
fn test_user_store() {
let temp_dir = TempDir::new().unwrap();
let store = UserStore::open(temp_dir.path()).unwrap();
assert!(store.is_empty());
let user = User::new("admin", "secret", UserRole::Admin).unwrap();
store.create(&user).unwrap();
assert!(!store.is_empty());
assert_eq!(store.count(), 1);
let loaded = store.get(&user.id).unwrap().unwrap();
assert_eq!(loaded.username, "admin");
let by_name = store.get_by_username("admin").unwrap().unwrap();
assert_eq!(by_name.id, user.id);
}
#[test]
fn test_user_roles() {
assert!(UserRole::Admin.is_admin());
assert!(!UserRole::Operator.is_admin());
assert!(!UserRole::Viewer.is_admin());
assert!(UserRole::Admin.can_manage_sessions());
assert!(UserRole::Operator.can_manage_sessions());
assert!(!UserRole::Viewer.can_manage_sessions());
}
#[test]
fn test_duplicate_user() {
let temp_dir = TempDir::new().unwrap();
let store = UserStore::open(temp_dir.path()).unwrap();
let user1 = User::new("admin", "secret1", UserRole::Admin).unwrap();
store.create(&user1).unwrap();
let user2 = User::new("admin", "secret2", UserRole::Operator).unwrap();
let result = store.create(&user2);
assert!(matches!(result, Err(AuthError::UserExists(_))));
}
}