use actix_identity::Identity;
use argon2::password_hash::SaltString;
use argon2::{Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
use rand::rngs::OsRng;
use uuid::Uuid;
use crate::db::Database;
use crate::models::User;
pub const SESSION_COOKIE_NAME: &str = "pear_session";
pub fn hash_password(password: &str) -> Result<String, String> {
let argon2 = Argon2::default();
let salt = SaltString::generate(&mut OsRng);
argon2
.hash_password(password.as_bytes(), &salt)
.map(|hash| hash.to_string())
.map_err(|e| e.to_string())
}
pub fn verify_password(password: &str, hash: &str) -> Result<bool, String> {
let argon2 = Argon2::default();
let parsed_hash = PasswordHash::new(hash).map_err(|e| e.to_string())?;
Ok(argon2
.verify_password(password.as_bytes(), &parsed_hash)
.is_ok())
}
pub fn create_user(username: &str, email: &str, password: &str) -> Result<User, String> {
let email = email.trim();
Ok(User {
id: Uuid::new_v4().to_string(),
username: username.trim().to_string(),
email: (!email.is_empty()).then(|| email.to_string()),
password_hash: hash_password(password)?,
created_at: chrono::Utc::now().to_rfc3339(),
})
}
/// Resolves the logged-in user from the Actix identity, if present.
pub async fn current_user(identity: Option<Identity>, db: &Database) -> Option<User> {
let user_id = identity?.id().ok()?;
db.get_user_by_id(&user_id).await.ok().flatten()
}
pub fn validate_username(username: &str) -> Result<(), String> {
let username = username.trim();
if username.len() < 3 {
return Err("Username must be at least 3 characters".to_string());
}
if username.len() > 32 {
return Err("Username must be at most 32 characters".to_string());
}
if !username
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
{
return Err("Username may only contain letters, numbers, '-' and '_'".to_string());
}
Ok(())
}
pub fn validate_email(email: &str) -> Result<(), String> {
let email = email.trim();
if email.is_empty() || !email.contains('@') {
return Err("Please enter a valid email address".to_string());
}
Ok(())
}
pub fn validate_password(password: &str) -> Result<(), String> {
if password.len() < 8 {
return Err("Password must be at least 8 characters".to_string());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hash_and_verify_password() {
let hash = hash_password("correct horse").unwrap();
assert!(verify_password("correct horse", &hash).unwrap());
assert!(!verify_password("wrong", &hash).unwrap());
}
#[test]
fn test_create_user_hashes_password_and_drops_blank_email() {
let user = create_user("alice", " ", "password123").unwrap();
assert_eq!(user.username, "alice");
assert_eq!(user.email, None);
assert!(verify_password("password123", &user.password_hash).unwrap());
let with_email = create_user("bob", "bob@example.com", "password123").unwrap();
assert_eq!(with_email.email, Some("bob@example.com".to_string()));
}
#[test]
fn test_validate_username() {
assert!(validate_username("alice").is_ok());
assert!(validate_username("a").is_err());
assert!(validate_username("al ice").is_err());
}
}