use std::collections::HashMap;
use std::sync::Arc;
use uuid::Uuid;
use super::dispatch::{JobDispatch, WorkflowDispatch};
#[derive(Debug, Clone)]
pub struct AuthContext {
user_id: Option<Uuid>,
roles: Vec<String>,
claims: HashMap<String, serde_json::Value>,
authenticated: bool,
}
impl AuthContext {
pub fn unauthenticated() -> Self {
Self {
user_id: None,
roles: Vec::new(),
claims: HashMap::new(),
authenticated: false,
}
}
pub fn authenticated(
user_id: Uuid,
roles: Vec<String>,
claims: HashMap<String, serde_json::Value>,
) -> Self {
Self {
user_id: Some(user_id),
roles,
claims,
authenticated: true,
}
}
pub fn is_authenticated(&self) -> bool {
self.authenticated
}
pub fn user_id(&self) -> Option<Uuid> {
self.user_id
}
pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
self.user_id
.ok_or_else(|| crate::error::ForgeError::Unauthorized("Authentication required".into()))
}
pub fn has_role(&self, role: &str) -> bool {
self.roles.iter().any(|r| r == role)
}
pub fn require_role(&self, role: &str) -> crate::error::Result<()> {
if self.has_role(role) {
Ok(())
} else {
Err(crate::error::ForgeError::Forbidden(format!(
"Required role '{}' not present",
role
)))
}
}
pub fn claim(&self, key: &str) -> Option<&serde_json::Value> {
self.claims.get(key)
}
pub fn roles(&self) -> &[String] {
&self.roles
}
}
#[derive(Debug, Clone)]
pub struct RequestMetadata {
pub request_id: Uuid,
pub trace_id: String,
pub client_ip: Option<String>,
pub user_agent: Option<String>,
pub timestamp: chrono::DateTime<chrono::Utc>,
}
impl RequestMetadata {
pub fn new() -> Self {
Self {
request_id: Uuid::new_v4(),
trace_id: Uuid::new_v4().to_string(),
client_ip: None,
user_agent: None,
timestamp: chrono::Utc::now(),
}
}
pub fn with_trace_id(trace_id: String) -> Self {
Self {
request_id: Uuid::new_v4(),
trace_id,
client_ip: None,
user_agent: None,
timestamp: chrono::Utc::now(),
}
}
}
impl Default for RequestMetadata {
fn default() -> Self {
Self::new()
}
}
pub struct QueryContext {
pub auth: AuthContext,
pub request: RequestMetadata,
db_pool: sqlx::PgPool,
}
impl QueryContext {
pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
Self {
auth,
request,
db_pool,
}
}
pub fn db(&self) -> &sqlx::PgPool {
&self.db_pool
}
pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
self.auth.require_user_id()
}
}
pub struct MutationContext {
pub auth: AuthContext,
pub request: RequestMetadata,
db_pool: sqlx::PgPool,
job_dispatch: Option<Arc<dyn JobDispatch>>,
workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
}
impl MutationContext {
pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
Self {
auth,
request,
db_pool,
job_dispatch: None,
workflow_dispatch: None,
}
}
pub fn with_dispatch(
db_pool: sqlx::PgPool,
auth: AuthContext,
request: RequestMetadata,
job_dispatch: Option<Arc<dyn JobDispatch>>,
workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
) -> Self {
Self {
auth,
request,
db_pool,
job_dispatch,
workflow_dispatch,
}
}
pub fn db(&self) -> &sqlx::PgPool {
&self.db_pool
}
pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
self.auth.require_user_id()
}
pub async fn dispatch_job<T: serde::Serialize>(
&self,
job_type: &str,
args: T,
) -> crate::error::Result<Uuid> {
let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
crate::error::ForgeError::Internal("Job dispatch not available".into())
})?;
let args_json = serde_json::to_value(args)?;
dispatcher.dispatch_by_name(job_type, args_json).await
}
pub async fn start_workflow<T: serde::Serialize>(
&self,
workflow_name: &str,
input: T,
) -> crate::error::Result<Uuid> {
let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
crate::error::ForgeError::Internal("Workflow dispatch not available".into())
})?;
let input_json = serde_json::to_value(input)?;
dispatcher.start_by_name(workflow_name, input_json).await
}
}
pub struct ActionContext {
pub auth: AuthContext,
pub request: RequestMetadata,
db_pool: sqlx::PgPool,
http_client: reqwest::Client,
job_dispatch: Option<Arc<dyn JobDispatch>>,
workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
}
impl ActionContext {
pub fn new(
db_pool: sqlx::PgPool,
auth: AuthContext,
request: RequestMetadata,
http_client: reqwest::Client,
) -> Self {
Self {
auth,
request,
db_pool,
http_client,
job_dispatch: None,
workflow_dispatch: None,
}
}
pub fn with_dispatch(
db_pool: sqlx::PgPool,
auth: AuthContext,
request: RequestMetadata,
http_client: reqwest::Client,
job_dispatch: Option<Arc<dyn JobDispatch>>,
workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
) -> Self {
Self {
auth,
request,
db_pool,
http_client,
job_dispatch,
workflow_dispatch,
}
}
pub fn db(&self) -> &sqlx::PgPool {
&self.db_pool
}
pub fn http(&self) -> &reqwest::Client {
&self.http_client
}
pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
self.auth.require_user_id()
}
pub async fn dispatch_job<T: serde::Serialize>(
&self,
job_type: &str,
args: T,
) -> crate::error::Result<Uuid> {
let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
crate::error::ForgeError::Internal("Job dispatch not available".into())
})?;
let args_json = serde_json::to_value(args)?;
dispatcher.dispatch_by_name(job_type, args_json).await
}
pub async fn start_workflow<T: serde::Serialize>(
&self,
workflow_name: &str,
input: T,
) -> crate::error::Result<Uuid> {
let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
crate::error::ForgeError::Internal("Workflow dispatch not available".into())
})?;
let input_json = serde_json::to_value(input)?;
dispatcher.start_by_name(workflow_name, input_json).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_auth_context_unauthenticated() {
let ctx = AuthContext::unauthenticated();
assert!(!ctx.is_authenticated());
assert!(ctx.user_id().is_none());
assert!(ctx.require_user_id().is_err());
}
#[test]
fn test_auth_context_authenticated() {
let user_id = Uuid::new_v4();
let ctx = AuthContext::authenticated(
user_id,
vec!["admin".to_string(), "user".to_string()],
HashMap::new(),
);
assert!(ctx.is_authenticated());
assert_eq!(ctx.user_id(), Some(user_id));
assert!(ctx.require_user_id().is_ok());
assert!(ctx.has_role("admin"));
assert!(ctx.has_role("user"));
assert!(!ctx.has_role("superadmin"));
assert!(ctx.require_role("admin").is_ok());
assert!(ctx.require_role("superadmin").is_err());
}
#[test]
fn test_auth_context_with_claims() {
let mut claims = HashMap::new();
claims.insert("org_id".to_string(), serde_json::json!("org-123"));
let ctx = AuthContext::authenticated(Uuid::new_v4(), vec![], claims);
assert_eq!(ctx.claim("org_id"), Some(&serde_json::json!("org-123")));
assert!(ctx.claim("nonexistent").is_none());
}
#[test]
fn test_request_metadata() {
let meta = RequestMetadata::new();
assert!(!meta.trace_id.is_empty());
assert!(meta.client_ip.is_none());
let meta2 = RequestMetadata::with_trace_id("trace-123".to_string());
assert_eq!(meta2.trace_id, "trace-123");
}
}