use chrono::{Duration, Utc}; use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation, Algorithm}; use serde::{Deserialize, Serialize}; /// JWT claims — must match `contracts::auth_middleware::Claims` field-for-field /// so that tokens generated here can be decoded there. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Claims { pub sub: String, // user_id (UUID string) pub email: String, pub roles: Vec, pub active_role: String, pub exp: usize, pub iat: usize, } pub struct JwtTokens { pub access_token: String, pub refresh_token: String, } /// Generate an access token + a random refresh token string. /// /// The refresh token is NOT a JWT — it is a random opaque string stored /// (hashed) in the `refresh_tokens` table. pub fn generate_tokens( user_id: String, email: String, roles: Vec, active_role: Option, jwt_secret: &str, ) -> anyhow::Result { let now = Utc::now(); let access_exp = now + Duration::minutes(15); let active_role = active_role .or_else(|| roles.first().cloned()) .unwrap_or_default(); let claims = Claims { sub: user_id, email, roles, active_role, iat: now.timestamp() as usize, exp: access_exp.timestamp() as usize, }; let access_token = encode( &Header::default(), &claims, &EncodingKey::from_secret(jwt_secret.as_bytes()), )?; // Refresh token is an opaque random string stored in DB, not a JWT. let refresh_token = uuid::Uuid::new_v4().to_string() + &uuid::Uuid::new_v4().to_string(); Ok(JwtTokens { access_token, refresh_token: refresh_token.replace('-', ""), }) } pub fn verify_access_token(token: &str, jwt_secret: &str) -> anyhow::Result { let token_data = decode::( token, &DecodingKey::from_secret(jwt_secret.as_bytes()), &Validation::new(Algorithm::HS256), )?; Ok(token_data.claims) }