use std::sync::Arc;
use argon2::password_hash::{PasswordHash, SaltString, rand_core::OsRng};
use argon2::{Argon2, PasswordHasher as Argon2PasswordHasher, PasswordVerifier};
use async_trait::async_trait;
use serde::Serialize;
use crate::adapters::DatabaseAdapter;
use crate::error::{AuthError, AuthResult};
use crate::plugin::AuthContext;
use crate::types::UpdateUser;
#[async_trait]
pub trait PasswordHasher: Send + Sync {
async fn hash(&self, password: &str) -> AuthResult<String>;
async fn verify(&self, hash: &str, password: &str) -> AuthResult<bool>;
}
pub async fn hash_password(
hasher: Option<&Arc<dyn PasswordHasher>>,
password: &str,
) -> AuthResult<String> {
if let Some(hasher) = hasher {
return hasher.hash(password).await;
}
let salt = SaltString::generate(&mut OsRng);
let argon2 = Argon2::default();
let password_hash = argon2
.hash_password(password.as_bytes(), &salt)
.map_err(|e| AuthError::PasswordHash(format!("Failed to hash password: {}", e)))?;
Ok(password_hash.to_string())
}
pub async fn verify_password(
hasher: Option<&Arc<dyn PasswordHasher>>,
password: &str,
hash: &str,
) -> AuthResult<()> {
if let Some(hasher) = hasher {
return hasher.verify(hash, password).await.and_then(|valid| {
if valid {
Ok(())
} else {
Err(AuthError::InvalidCredentials)
}
});
}
let parsed_hash = PasswordHash::new(hash)
.map_err(|e| AuthError::PasswordHash(format!("Invalid password hash: {}", e)))?;
let argon2 = Argon2::default();
argon2
.verify_password(password.as_bytes(), &parsed_hash)
.map_err(|_| AuthError::InvalidCredentials)?;
Ok(())
}
pub fn validate_password<DB: DatabaseAdapter>(
password: &str,
min_length: usize,
max_length: usize,
ctx: &AuthContext<DB>,
) -> AuthResult<()> {
let config = &ctx.config.password;
if password.len() < min_length {
return Err(AuthError::bad_request(format!(
"Password must be at least {} characters long",
config.min_length
)));
}
if password.len() > max_length {
return Err(AuthError::bad_request(format!(
"Password must be at most {} characters long",
max_length
)));
}
if config.require_uppercase && !password.chars().any(|c| c.is_uppercase()) {
return Err(AuthError::bad_request(
"Password must contain at least one uppercase letter",
));
}
if config.require_lowercase && !password.chars().any(|c| c.is_lowercase()) {
return Err(AuthError::bad_request(
"Password must contain at least one lowercase letter",
));
}
if config.require_numbers && !password.chars().any(|c| c.is_ascii_digit()) {
return Err(AuthError::bad_request(
"Password must contain at least one number",
));
}
if config.require_special
&& !password
.chars()
.any(|c| "!@#$%^&*()_+-=[]{}|;:,.<>?".contains(c))
{
return Err(AuthError::bad_request(
"Password must contain at least one special character",
));
}
Ok(())
}
pub fn serialize_to_value(value: &impl Serialize) -> AuthResult<serde_json::Value> {
serde_json::to_value(value)
.map_err(|e| AuthError::internal(format!("Failed to serialize value: {}", e)))
}
pub fn update_user_metadata(metadata: serde_json::Value) -> UpdateUser {
UpdateUser {
metadata: Some(metadata),
..Default::default()
}
}