use async_trait::async_trait;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AuthContext {
pub user_id: String,
pub session_id: String,
pub roles: Vec<String>,
pub metadata: Value,
}
impl AuthContext {
pub fn new(user_id: String, session_id: String, roles: Vec<String>, metadata: Value) -> Self {
Self {
user_id,
session_id,
roles,
metadata,
}
}
pub fn anonymous() -> Self {
Self {
user_id: "anonymous".to_string(),
session_id: String::new(),
roles: vec![],
metadata: Value::Null,
}
}
pub fn is_authenticated(&self) -> bool {
self.user_id != "anonymous" && !self.session_id.is_empty()
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn get_metadata_string(&self, key: &str) -> Option<String> {
self.metadata.get(key).and_then(|v| v.as_str()).map(String::from)
}
pub fn tenant(&self) -> Option<String> {
self.get_metadata_string("tenant_id")
.or_else(|| self.get_metadata_string("realm"))
}
}
#[async_trait]
pub trait SessionValidator: Send + Sync + 'static {
async fn validate(&self, cookie_value: &str) -> Option<AuthContext>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_context_creation() {
let ctx = AuthContext::new(
"user-123".to_string(),
"sess-456".to_string(),
vec!["admin".to_string()],
serde_json::json!({"tenant_id": "acme"}),
);
assert_eq!(ctx.user_id, "user-123");
assert_eq!(ctx.session_id, "sess-456");
assert!(ctx.has_role("admin"));
assert!(!ctx.has_role("user"));
assert_eq!(ctx.tenant(), Some("acme".to_string()));
assert!(ctx.is_authenticated());
}
#[test]
fn test_auth_context_clone() {
let ctx = AuthContext::new(
"alice".to_string(),
"sess-1".to_string(),
vec!["admin".to_string()],
serde_json::json!({"org": "acme"}),
);
let cloned = ctx.clone();
assert_eq!(ctx.user_id, cloned.user_id);
assert_eq!(ctx.session_id, cloned.session_id);
assert_eq!(ctx.roles, cloned.roles);
}
#[test]
fn test_anonymous_context() {
let ctx = AuthContext::anonymous();
assert_eq!(ctx.user_id, "anonymous");
assert!(!ctx.is_authenticated());
assert!(ctx.roles.is_empty());
}
#[test]
fn test_role_checking() {
let ctx = AuthContext::new(
"user-1".to_string(),
"sess-1".to_string(),
vec!["user".to_string(), "editor".to_string()],
Value::Null,
);
assert!(ctx.has_role("user"));
assert!(ctx.has_role("editor"));
assert!(!ctx.has_role("admin"));
}
#[test]
fn test_metadata_access() {
let ctx = AuthContext::new(
"user-1".to_string(),
"sess-1".to_string(),
vec![],
serde_json::json!({
"tenant_id": "org-123",
"realm": "production",
"email": "user@example.com"
}),
);
assert_eq!(ctx.get_metadata_string("tenant_id"), Some("org-123".to_string()));
assert_eq!(ctx.get_metadata_string("realm"), Some("production".to_string()));
assert_eq!(ctx.get_metadata_string("email"), Some("user@example.com".to_string()));
assert_eq!(ctx.get_metadata_string("nonexistent"), None);
}
#[test]
fn test_tenant_from_metadata() {
let ctx1 = AuthContext::new(
"user-1".to_string(),
"sess-1".to_string(),
vec![],
serde_json::json!({"tenant_id": "org-123", "realm": "prod"}),
);
assert_eq!(ctx1.tenant(), Some("org-123".to_string()));
let ctx2 = AuthContext::new(
"user-1".to_string(),
"sess-1".to_string(),
vec![],
serde_json::json!({"realm": "prod"}),
);
assert_eq!(ctx2.tenant(), Some("prod".to_string()));
let ctx3 = AuthContext::new(
"user-1".to_string(),
"sess-1".to_string(),
vec![],
Value::Null,
);
assert_eq!(ctx3.tenant(), None);
}
}