use anyhow::{Context, Result}; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2, }; use chrono::Utc; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; use uuid::Uuid; use crate::config::AppConfig; use crate::models::auth::Claims; /// Hash a password using Argon2id. pub fn hash_password(password: &str) -> Result { let salt = SaltString::generate(&mut OsRng); let argon2 = Argon2::default(); let hash = argon2 .hash_password(password.as_bytes(), &salt) .map_err(|e| anyhow::anyhow!("Password hashing failed: {e}"))?; Ok(hash.to_string()) } /// Verify a password against an Argon2id hash. pub fn verify_password(password: &str, hash: &str) -> Result { let parsed_hash = PasswordHash::new(hash) .map_err(|e| anyhow::anyhow!("Invalid password hash: {e}"))?; Ok(Argon2::default() .verify_password(password.as_bytes(), &parsed_hash) .is_ok()) } /// Create a JWT access token. pub fn create_access_token( config: &AppConfig, user_id: Uuid, email: &str, license_id: Option, role: Option, is_super_admin: bool, ) -> Result { let now = Utc::now().timestamp(); let claims = Claims { sub: user_id, email: email.to_string(), license_id, role, is_super_admin, exp: now + config.jwt_access_expiry_seconds, iat: now, token_type: "access".to_string(), }; encode( &Header::default(), &claims, &EncodingKey::from_secret(config.jwt_secret.as_bytes()), ) .context("Failed to create access token") } /// Create a JWT refresh token (longer-lived). pub fn create_refresh_token( config: &AppConfig, user_id: Uuid, email: &str, ) -> Result { let now = Utc::now().timestamp(); let claims = Claims { sub: user_id, email: email.to_string(), license_id: None, role: None, is_super_admin: false, exp: now + config.jwt_refresh_expiry_seconds, iat: now, token_type: "refresh".to_string(), }; encode( &Header::default(), &claims, &EncodingKey::from_secret(config.jwt_secret.as_bytes()), ) .context("Failed to create refresh token") } /// Validate and decode a JWT token. Returns the claims on success. pub fn validate_token(config: &AppConfig, token: &str) -> Result { let token_data = decode::( token, &DecodingKey::from_secret(config.jwt_secret.as_bytes()), &Validation::default(), ) .context("Invalid or expired token")?; Ok(token_data.claims) }