use argon2::{
Argon2,
password_hash::{
self, PasswordHash, PasswordHasher, PasswordVerifier, SaltString, rand_core::OsRng,
},
};
use chrono::Utc;
use rkyv::{Archive, Deserialize, Serialize};
use crate::model::error::Error;
#[derive(Debug, Archive, Serialize, Deserialize, Default)]
pub struct User {
username: String,
password_hash: String,
last_activity: Option<i64>,
}
impl User {
pub fn new(
username: impl Into<String>,
password: impl Into<String>,
) -> Result<Self, password_hash::Error> {
let password = password.into();
let username = username.into();
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)?
.to_string();
Ok(Self {
username,
password_hash,
last_activity: None,
})
}
pub fn username(&self) -> &str {
&self.username
}
pub fn check(&self, password: impl AsRef<str>) -> Result<bool, Error> {
let parsed_hash = PasswordHash::new(&self.password_hash).map_err(Error::runtime)?;
let argon2 = Argon2::default();
match argon2.verify_password(password.as_ref().as_bytes(), &parsed_hash) {
Ok(_) => Ok(true),
Err(password_hash::Error::Password) => Ok(false),
Err(e) => Err(Error::runtime(e)),
}
}
pub fn set_last_activity(&mut self, timestamp: i64) {
self.last_activity = Some(timestamp);
}
pub fn last_activity(&self) -> Option<i64> {
self.last_activity
}
pub fn touch(&mut self) {
self.last_activity = Some(Utc::now().timestamp_millis());
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_creation() {
let user = User::new("alice", "password123").expect("Failed to create user");
assert_eq!(user.username(), "alice");
assert_eq!(user.last_activity(), None);
}
#[test]
fn test_username() {
let user = User::new("bob", "secret").expect("Failed to create user");
assert_eq!(user.username(), "bob");
}
#[test]
fn test_password_verification_success() {
let user = User::new("alice", "correct_password").expect("Failed to create user");
assert!(
user.check("correct_password").is_ok(),
"Password verification should succeed with correct password"
);
}
#[test]
fn test_password_verification_failure() {
let user = User::new("alice", "correct_password").expect("Failed to create user");
assert!(
!user
.check("wrong_password")
.expect("Failed to check password"),
"Password verification should fail with incorrect password"
);
}
#[test]
fn test_password_is_hashed() {
let password = "plaintext_password";
let user = User::new("alice", password).expect("Failed to create user");
assert!(
!user.password_hash.contains(password),
"Password should be hashed, not stored in plaintext"
);
assert!(
user.password_hash.starts_with("$argon2"),
"Password hash should be in Argon2 format"
);
}
#[test]
fn test_unique_salt_per_user() {
let password = "same_password";
let user1 = User::new("alice", password).expect("Failed to create user1");
let user2 = User::new("bob", password).expect("Failed to create user2");
assert_ne!(
user1.password_hash, user2.password_hash,
"Different users with same password should have different hashes"
);
}
#[test]
fn test_last_activity_initial() {
let user = User::new("alice", "password").expect("Failed to create user");
assert_eq!(
user.last_activity(),
None,
"New user should have no last activity"
);
}
#[test]
fn test_set_last_activity() {
let mut user = User::new("alice", "password").expect("Failed to create user");
let timestamp = 1609459200000i64;
user.set_last_activity(timestamp);
assert_eq!(user.last_activity(), Some(timestamp));
}
#[test]
fn test_touch_updates_activity() {
let mut user = User::new("alice", "password").expect("Failed to create user");
assert_eq!(user.last_activity(), None);
user.touch();
assert!(
user.last_activity().is_some(),
"touch() should set last_activity"
);
let now = Utc::now().timestamp_millis();
let activity = user.last_activity().unwrap();
assert!(
(now - activity).abs() < 1000,
"touch() should set timestamp to current time (within 1 second)"
);
}
#[test]
fn test_touch_updates_timestamp() {
let mut user = User::new("alice", "password").expect("Failed to create user");
user.touch();
let first_activity = user
.last_activity()
.expect("Should have activity after first touch");
std::thread::sleep(std::time::Duration::from_millis(10));
user.touch();
let second_activity = user
.last_activity()
.expect("Should have activity after second touch");
assert!(
second_activity > first_activity,
"Subsequent touch() should update timestamp to a later time"
);
}
}