this repo has no description
at main 103 lines 3.3 kB view raw
1use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; 2use argon2::Argon2; 3use axum::extract::FromRequestParts; 4use axum::http::request::Parts; 5use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation}; 6use serde::{Deserialize, Serialize}; 7 8use crate::config::AppState; 9use crate::errors::AppError; 10 11// --------------------------------------------------------------------------- 12// Password hashing 13// --------------------------------------------------------------------------- 14 15pub fn hash_password(password: &str) -> String { 16 // Use UUID v4 random bytes as salt material (uuid is a direct dependency). 17 let salt_bytes = uuid::Uuid::new_v4().into_bytes(); 18 let salt = SaltString::encode_b64(&salt_bytes).expect("Failed to encode salt"); 19 let argon2 = Argon2::default(); 20 argon2 21 .hash_password(password.as_bytes(), &salt) 22 .expect("Failed to hash password") 23 .to_string() 24} 25 26pub fn verify_password(password: &str, hash: &str) -> bool { 27 let parsed = match PasswordHash::new(hash) { 28 Ok(h) => h, 29 Err(_) => return false, 30 }; 31 Argon2::default() 32 .verify_password(password.as_bytes(), &parsed) 33 .is_ok() 34} 35 36// --------------------------------------------------------------------------- 37// JWT 38// --------------------------------------------------------------------------- 39 40#[derive(Debug, Serialize, Deserialize)] 41struct Claims { 42 sub: String, 43 exp: usize, 44} 45 46pub fn create_token(user_id: &str, secret: &str) -> String { 47 let expiration = chrono::Utc::now() 48 .checked_add_signed(chrono::Duration::days(30)) 49 .expect("valid timestamp") 50 .timestamp() as usize; 51 52 let claims = Claims { 53 sub: user_id.to_string(), 54 exp: expiration, 55 }; 56 57 encode( 58 &Header::default(), 59 &claims, 60 &EncodingKey::from_secret(secret.as_bytes()), 61 ) 62 .expect("Failed to create JWT token") 63} 64 65fn decode_token(token: &str, secret: &str) -> Result<String, AppError> { 66 let data = decode::<Claims>( 67 token, 68 &DecodingKey::from_secret(secret.as_bytes()), 69 &Validation::default(), 70 ) 71 .map_err(|e| AppError::Unauthorized(format!("Invalid token: {e}")))?; 72 73 Ok(data.claims.sub) 74} 75 76// --------------------------------------------------------------------------- 77// AuthUser extractor 78// --------------------------------------------------------------------------- 79 80/// Extracts the authenticated user's ID from the Authorization header. 81pub struct AuthUser(pub String); 82 83impl FromRequestParts<AppState> for AuthUser { 84 type Rejection = AppError; 85 86 async fn from_request_parts( 87 parts: &mut Parts, 88 state: &AppState, 89 ) -> Result<Self, Self::Rejection> { 90 let header = parts 91 .headers 92 .get("Authorization") 93 .and_then(|v| v.to_str().ok()) 94 .ok_or_else(|| AppError::Unauthorized("Missing Authorization header".to_string()))?; 95 96 let token = header 97 .strip_prefix("Bearer ") 98 .ok_or_else(|| AppError::Unauthorized("Invalid Authorization format".to_string()))?; 99 100 let user_id = decode_token(token, &state.jwt_secret)?; 101 Ok(AuthUser(user_id)) 102 } 103}