use std::sync::atomic::{AtomicU64, Ordering};
use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString},
Argon2,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct SessionId(pub u64);
impl SessionId {
pub fn new() -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(1);
Self(COUNTER.fetch_add(1, Ordering::SeqCst))
}
}
impl Default for SessionId {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct UserId(pub u64);
#[derive(Debug, Clone)]
pub struct User {
pub id: UserId,
pub name: String,
pub password_hash: Option<String>,
}
impl User {
pub fn new(name: impl Into<String>, password: impl Into<String>) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(1);
let password_str = password.into();
let password_hash = if password_str.is_empty() {
None
} else {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
argon2
.hash_password(password_str.as_bytes(), &salt)
.ok()
.map(|hash| hash.to_string())
};
Self {
id: UserId(COUNTER.fetch_add(1, Ordering::SeqCst)),
name: name.into(),
password_hash,
}
}
pub fn new_passwordless(name: impl Into<String>) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(1);
Self {
id: UserId(COUNTER.fetch_add(1, Ordering::SeqCst)),
name: name.into(),
password_hash: None,
}
}
pub fn verify_password(&self, password: &str) -> bool {
match &self.password_hash {
Some(hash_str) => {
if let Ok(parsed_hash) = PasswordHash::new(hash_str) {
Argon2::default()
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok()
} else {
false
}
}
None => false,
}
}
pub fn has_password(&self) -> bool {
self.password_hash.is_some()
}
pub fn set_password(&mut self, password: &str) {
if password.is_empty() {
self.password_hash = None;
} else {
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
self.password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.ok()
.map(|hash| hash.to_string());
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IsolationLevel {
ReadCommitted,
RepeatableRead,
Serializable,
}
impl IsolationLevel {
#[allow(non_upper_case_globals)]
pub const Snapshot: Self = Self::RepeatableRead;
}
impl Default for IsolationLevel {
fn default() -> Self {
Self::ReadCommitted
}
}
#[derive(Debug, Clone)]
pub struct Session {
pub id: SessionId,
pub user_id: UserId,
pub isolation_level: IsolationLevel,
pub active_txn: Option<u64>,
pub created_at: u64,
pub last_activity: u64,
pub stats: SessionStats,
}
impl Session {
pub fn new(user_id: UserId, isolation_level: IsolationLevel) -> Self {
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
Self {
id: SessionId::new(),
user_id,
isolation_level,
active_txn: None,
created_at: now,
last_activity: now,
stats: SessionStats::default(),
}
}
pub fn touch(&mut self) {
self.last_activity = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
}
}
#[derive(Debug, Clone, Default)]
pub struct SessionStats {
pub transactions_started: u64,
pub transactions_committed: u64,
pub transactions_aborted: u64,
pub queries_executed: u64,
pub bytes_read: u64,
pub bytes_written: u64,
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn test_user_password_hashing() {
let user = User::new("alice", "secret123");
assert!(user.has_password());
assert!(user.verify_password("secret123"));
assert!(!user.verify_password("wrongpassword"));
assert!(!user.verify_password(""));
}
#[test]
fn test_user_empty_password() {
let user = User::new("bob", "");
assert!(!user.has_password());
assert!(!user.verify_password(""));
assert!(!user.verify_password("anypassword"));
}
#[test]
fn test_user_passwordless() {
let user = User::new_passwordless("system");
assert!(!user.has_password());
assert!(!user.verify_password("anypassword"));
}
#[test]
fn test_user_set_password() {
let mut user = User::new_passwordless("charlie");
assert!(!user.has_password());
user.set_password("newpassword");
assert!(user.has_password());
assert!(user.verify_password("newpassword"));
user.set_password("");
assert!(!user.has_password());
}
#[test]
fn test_user_unique_ids() {
let user1 = User::new("user1", "pass1");
let user2 = User::new("user2", "pass2");
assert_ne!(user1.id, user2.id);
}
#[test]
fn test_session_creation() {
let user = User::new("testuser", "testpass");
let session = Session::new(user.id, IsolationLevel::ReadCommitted);
assert_eq!(session.user_id, user.id);
assert_eq!(session.isolation_level, IsolationLevel::ReadCommitted);
assert!(session.active_txn.is_none());
}
}