use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum OneOrMany {
One(String),
Many(Vec<String>),
}
impl OneOrMany {
pub fn contains(&self, value: &str) -> bool {
match self {
Self::One(s) => s == value,
Self::Many(v) => v.iter().any(|s| s == value),
}
}
pub fn to_vec(&self) -> Vec<String> {
match self {
Self::One(s) => vec![s.clone()],
Self::Many(v) => v.clone(),
}
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct StandardClaims {
pub sub: String,
pub iss: String,
#[serde(default)]
pub aud: Option<OneOrMany>,
pub exp: i64,
#[serde(default)]
pub nbf: Option<i64>,
#[serde(default)]
pub iat: Option<i64>,
#[serde(default)]
pub jti: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Default)]
pub struct CustomClaims {
#[serde(default)]
pub tenant_id: Option<String>,
#[serde(default)]
pub roles: Vec<String>,
#[serde(default)]
pub email: Option<String>,
#[serde(default)]
pub name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct JwtClaims {
#[serde(flatten)]
pub standard: StandardClaims,
#[serde(flatten)]
pub custom: CustomClaims,
}
#[derive(Debug, Clone)]
pub struct AuthenticatedUser {
pub sub: String,
pub email: Option<String>,
pub name: Option<String>,
pub tenant_id: Option<String>,
pub tenant_schema: Option<String>,
pub roles: Vec<String>,
}
impl AuthenticatedUser {
pub fn from_claims(claims: &JwtClaims, tenant_schema: Option<String>) -> Self {
Self {
sub: claims.standard.sub.clone(),
email: claims.custom.email.clone(),
name: claims.custom.name.clone(),
tenant_id: claims.custom.tenant_id.clone(),
tenant_schema,
roles: claims.custom.roles.clone(),
}
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_one_or_many_one() {
let aud = OneOrMany::One("api".to_string());
assert!(aud.contains("api"));
assert!(!aud.contains("other"));
assert_eq!(aud.to_vec(), vec!["api"]);
}
#[test]
fn test_one_or_many_many() {
let aud = OneOrMany::Many(vec!["api".to_string(), "web".to_string()]);
assert!(aud.contains("api"));
assert!(aud.contains("web"));
assert!(!aud.contains("other"));
assert_eq!(aud.to_vec(), vec!["api", "web"]);
}
#[test]
fn test_deserialize_claims_single_audience() {
let json = r#"{
"sub": "user123",
"iss": "https://auth.example.com",
"aud": "api",
"exp": 1700000000
}"#;
let claims: JwtClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.standard.sub, "user123");
assert!(claims.standard.aud.as_ref().unwrap().contains("api"));
}
#[test]
fn test_deserialize_claims_multiple_audience() {
let json = r#"{
"sub": "user123",
"iss": "https://auth.example.com",
"aud": ["api", "web"],
"exp": 1700000000
}"#;
let claims: JwtClaims = serde_json::from_str(json).unwrap();
assert!(claims.standard.aud.as_ref().unwrap().contains("api"));
assert!(claims.standard.aud.as_ref().unwrap().contains("web"));
}
#[test]
fn test_deserialize_custom_claims() {
let json = r#"{
"sub": "user123",
"iss": "https://auth.example.com",
"exp": 1700000000,
"tenant_id": "tenant1",
"roles": ["admin", "user"],
"email": "user@example.com",
"name": "Test User"
}"#;
let claims: JwtClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.custom.tenant_id, Some("tenant1".to_string()));
assert_eq!(claims.custom.roles, vec!["admin", "user"]);
assert_eq!(claims.custom.email, Some("user@example.com".to_string()));
}
#[test]
fn test_deserialize_minimal_claims() {
let json = r#"{
"sub": "user123",
"iss": "https://auth.example.com",
"exp": 1700000000
}"#;
let claims: JwtClaims = serde_json::from_str(json).unwrap();
assert_eq!(claims.standard.sub, "user123");
assert!(claims.custom.tenant_id.is_none());
assert!(claims.custom.roles.is_empty());
}
#[test]
fn test_authenticated_user_from_claims() {
let json = r#"{
"sub": "user123",
"iss": "https://auth.example.com",
"exp": 1700000000,
"tenant_id": "tenant1",
"roles": ["admin"],
"email": "user@example.com"
}"#;
let claims: JwtClaims = serde_json::from_str(json).unwrap();
let user = AuthenticatedUser::from_claims(&claims, Some("TENANT1".to_string()));
assert_eq!(user.sub, "user123");
assert_eq!(user.tenant_id, Some("tenant1".to_string()));
assert_eq!(user.tenant_schema, Some("TENANT1".to_string()));
assert!(user.has_role("admin"));
assert!(!user.has_role("user"));
}
}