use crate::{CorteqError, Result};
use jsonwebtoken::{DecodingKey, EncodingKey, Header, Validation};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Claims {
pub sub: Uuid,
pub tenant_id: Uuid,
pub roles: Vec<String>,
pub exp: i64,
pub iat: i64,
}
#[derive(Clone)]
pub struct JwtService {
encoding_key: EncodingKey,
decoding_key: DecodingKey,
validation: Validation,
}
impl std::fmt::Debug for JwtService {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JwtService")
.field("validation", &self.validation)
.finish_non_exhaustive()
}
}
impl JwtService {
pub fn new(secret: &[u8]) -> Self {
Self {
encoding_key: EncodingKey::from_secret(secret),
decoding_key: DecodingKey::from_secret(secret),
validation: Validation::default(),
}
}
pub fn encode(&self, claims: &Claims) -> Result<String> {
jsonwebtoken::encode(&Header::default(), claims, &self.encoding_key)
.map_err(CorteqError::from)
}
pub fn decode(&self, token: &str) -> Result<Claims> {
let token_data =
jsonwebtoken::decode::<Claims>(token, &self.decoding_key, &self.validation)?;
Ok(token_data.claims)
}
pub fn validate_tenant(&self, token: &str, expected_tenant_id: Uuid) -> Result<Claims> {
let claims = self.decode(token)?;
if claims.tenant_id != expected_tenant_id {
return Err(CorteqError::AuthorizationFailed(
"Token tenant mismatch".to_string(),
));
}
Ok(claims)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Utc;
#[test]
fn test_jwt_encode_decode() {
let service = JwtService::new(b"test-secret");
let claims = Claims {
sub: Uuid::new_v4(),
tenant_id: Uuid::new_v4(),
roles: vec!["user".to_string()],
exp: (Utc::now() + chrono::Duration::hours(1)).timestamp(),
iat: Utc::now().timestamp(),
};
let token = service.encode(&claims).unwrap();
let decoded = service.decode(&token).unwrap();
assert_eq!(claims.sub, decoded.sub);
assert_eq!(claims.tenant_id, decoded.tenant_id);
}
#[test]
fn test_tenant_validation() {
let service = JwtService::new(b"test-secret");
let tenant_id = Uuid::new_v4();
let claims = Claims {
sub: Uuid::new_v4(),
tenant_id,
roles: vec!["user".to_string()],
exp: (Utc::now() + chrono::Duration::hours(1)).timestamp(),
iat: Utc::now().timestamp(),
};
let token = service.encode(&claims).unwrap();
assert!(service.validate_tenant(&token, tenant_id).is_ok());
let wrong_tenant = Uuid::new_v4();
assert!(service.validate_tenant(&token, wrong_tenant).is_err());
}
}