- gateway, companies, customers, job_seekers apps updated - users config/mod/mail handlers - auth middleware and jwt crate updates - db models: user, config, mod updates - all remaining migrations: portfolio, notifications, reviews, kb, support, coupons, onboarding states Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
71 lines
2 KiB
Rust
71 lines
2 KiB
Rust
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<String>,
|
|
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<String>,
|
|
active_role: Option<String>,
|
|
jwt_secret: &str,
|
|
) -> anyhow::Result<JwtTokens> {
|
|
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<Claims> {
|
|
let token_data = decode::<Claims>(
|
|
token,
|
|
&DecodingKey::from_secret(jwt_secret.as_bytes()),
|
|
&Validation::new(Algorithm::HS256),
|
|
)?;
|
|
Ok(token_data.claims)
|
|
}
|