this repo has no description
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}