use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; use argon2::Argon2; use axum::extract::FromRequestParts; use axum::http::request::Parts; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; use crate::config::AppState; use crate::errors::AppError; // --------------------------------------------------------------------------- // Password hashing // --------------------------------------------------------------------------- pub fn hash_password(password: &str) -> String { // Use UUID v4 random bytes as salt material (uuid is a direct dependency). let salt_bytes = uuid::Uuid::new_v4().into_bytes(); let salt = SaltString::encode_b64(&salt_bytes).expect("Failed to encode salt"); let argon2 = Argon2::default(); argon2 .hash_password(password.as_bytes(), &salt) .expect("Failed to hash password") .to_string() } pub fn verify_password(password: &str, hash: &str) -> bool { let parsed = match PasswordHash::new(hash) { Ok(h) => h, Err(_) => return false, }; Argon2::default() .verify_password(password.as_bytes(), &parsed) .is_ok() } // --------------------------------------------------------------------------- // JWT // --------------------------------------------------------------------------- #[derive(Debug, Serialize, Deserialize)] struct Claims { sub: String, exp: usize, } pub fn create_token(user_id: &str, secret: &str) -> String { let expiration = chrono::Utc::now() .checked_add_signed(chrono::Duration::days(30)) .expect("valid timestamp") .timestamp() as usize; let claims = Claims { sub: user_id.to_string(), exp: expiration, }; encode( &Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()), ) .expect("Failed to create JWT token") } fn decode_token(token: &str, secret: &str) -> Result { let data = decode::( token, &DecodingKey::from_secret(secret.as_bytes()), &Validation::default(), ) .map_err(|e| AppError::Unauthorized(format!("Invalid token: {e}")))?; Ok(data.claims.sub) } // --------------------------------------------------------------------------- // AuthUser extractor // --------------------------------------------------------------------------- /// Extracts the authenticated user's ID from the Authorization header. pub struct AuthUser(pub String); impl FromRequestParts for AuthUser { type Rejection = AppError; async fn from_request_parts( parts: &mut Parts, state: &AppState, ) -> Result { let header = parts .headers .get("Authorization") .and_then(|v| v.to_str().ok()) .ok_or_else(|| AppError::Unauthorized("Missing Authorization header".to_string()))?; let token = header .strip_prefix("Bearer ") .ok_or_else(|| AppError::Unauthorized("Invalid Authorization format".to_string()))?; let user_id = decode_token(token, &state.jwt_secret)?; Ok(AuthUser(user_id)) } }