1use std::collections::HashMap;
2use std::sync::Arc;
3
4use uuid::Uuid;
5
6use super::dispatch::{JobDispatch, WorkflowDispatch};
7use crate::env::{EnvAccess, EnvProvider, RealEnvProvider};
8
9#[derive(Debug, Clone)]
11pub struct AuthContext {
12 user_id: Option<Uuid>,
14 roles: Vec<String>,
16 claims: HashMap<String, serde_json::Value>,
18 authenticated: bool,
20}
21
22impl AuthContext {
23 pub fn unauthenticated() -> Self {
25 Self {
26 user_id: None,
27 roles: Vec::new(),
28 claims: HashMap::new(),
29 authenticated: false,
30 }
31 }
32
33 pub fn authenticated(
35 user_id: Uuid,
36 roles: Vec<String>,
37 claims: HashMap<String, serde_json::Value>,
38 ) -> Self {
39 Self {
40 user_id: Some(user_id),
41 roles,
42 claims,
43 authenticated: true,
44 }
45 }
46
47 pub fn is_authenticated(&self) -> bool {
49 self.authenticated
50 }
51
52 pub fn user_id(&self) -> Option<Uuid> {
54 self.user_id
55 }
56
57 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
59 self.user_id
60 .ok_or_else(|| crate::error::ForgeError::Unauthorized("Authentication required".into()))
61 }
62
63 pub fn has_role(&self, role: &str) -> bool {
65 self.roles.iter().any(|r| r == role)
66 }
67
68 pub fn require_role(&self, role: &str) -> crate::error::Result<()> {
70 if self.has_role(role) {
71 Ok(())
72 } else {
73 Err(crate::error::ForgeError::Forbidden(format!(
74 "Required role '{}' not present",
75 role
76 )))
77 }
78 }
79
80 pub fn claim(&self, key: &str) -> Option<&serde_json::Value> {
82 self.claims.get(key)
83 }
84
85 pub fn roles(&self) -> &[String] {
87 &self.roles
88 }
89}
90
91#[derive(Debug, Clone)]
93pub struct RequestMetadata {
94 pub request_id: Uuid,
96 pub trace_id: String,
98 pub client_ip: Option<String>,
100 pub user_agent: Option<String>,
102 pub timestamp: chrono::DateTime<chrono::Utc>,
104}
105
106impl RequestMetadata {
107 pub fn new() -> Self {
109 Self {
110 request_id: Uuid::new_v4(),
111 trace_id: Uuid::new_v4().to_string(),
112 client_ip: None,
113 user_agent: None,
114 timestamp: chrono::Utc::now(),
115 }
116 }
117
118 pub fn with_trace_id(trace_id: String) -> Self {
120 Self {
121 request_id: Uuid::new_v4(),
122 trace_id,
123 client_ip: None,
124 user_agent: None,
125 timestamp: chrono::Utc::now(),
126 }
127 }
128}
129
130impl Default for RequestMetadata {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136pub struct QueryContext {
138 pub auth: AuthContext,
140 pub request: RequestMetadata,
142 db_pool: sqlx::PgPool,
144 env_provider: Arc<dyn EnvProvider>,
146}
147
148impl QueryContext {
149 pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
151 Self {
152 auth,
153 request,
154 db_pool,
155 env_provider: Arc::new(RealEnvProvider::new()),
156 }
157 }
158
159 pub fn with_env(
161 db_pool: sqlx::PgPool,
162 auth: AuthContext,
163 request: RequestMetadata,
164 env_provider: Arc<dyn EnvProvider>,
165 ) -> Self {
166 Self {
167 auth,
168 request,
169 db_pool,
170 env_provider,
171 }
172 }
173
174 pub fn db(&self) -> &sqlx::PgPool {
176 &self.db_pool
177 }
178
179 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
181 self.auth.require_user_id()
182 }
183}
184
185impl EnvAccess for QueryContext {
186 fn env_provider(&self) -> &dyn EnvProvider {
187 self.env_provider.as_ref()
188 }
189}
190
191pub struct MutationContext {
193 pub auth: AuthContext,
195 pub request: RequestMetadata,
197 db_pool: sqlx::PgPool,
199 job_dispatch: Option<Arc<dyn JobDispatch>>,
201 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
203 env_provider: Arc<dyn EnvProvider>,
205}
206
207impl MutationContext {
208 pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
210 Self {
211 auth,
212 request,
213 db_pool,
214 job_dispatch: None,
215 workflow_dispatch: None,
216 env_provider: Arc::new(RealEnvProvider::new()),
217 }
218 }
219
220 pub fn with_dispatch(
222 db_pool: sqlx::PgPool,
223 auth: AuthContext,
224 request: RequestMetadata,
225 job_dispatch: Option<Arc<dyn JobDispatch>>,
226 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
227 ) -> Self {
228 Self {
229 auth,
230 request,
231 db_pool,
232 job_dispatch,
233 workflow_dispatch,
234 env_provider: Arc::new(RealEnvProvider::new()),
235 }
236 }
237
238 pub fn with_env(
240 db_pool: sqlx::PgPool,
241 auth: AuthContext,
242 request: RequestMetadata,
243 job_dispatch: Option<Arc<dyn JobDispatch>>,
244 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
245 env_provider: Arc<dyn EnvProvider>,
246 ) -> Self {
247 Self {
248 auth,
249 request,
250 db_pool,
251 job_dispatch,
252 workflow_dispatch,
253 env_provider,
254 }
255 }
256
257 pub fn db(&self) -> &sqlx::PgPool {
259 &self.db_pool
260 }
261
262 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
264 self.auth.require_user_id()
265 }
266
267 pub async fn dispatch_job<T: serde::Serialize>(
276 &self,
277 job_type: &str,
278 args: T,
279 ) -> crate::error::Result<Uuid> {
280 let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
281 crate::error::ForgeError::Internal("Job dispatch not available".into())
282 })?;
283 let args_json = serde_json::to_value(args)?;
284 dispatcher.dispatch_by_name(job_type, args_json).await
285 }
286
287 pub async fn start_workflow<T: serde::Serialize>(
296 &self,
297 workflow_name: &str,
298 input: T,
299 ) -> crate::error::Result<Uuid> {
300 let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
301 crate::error::ForgeError::Internal("Workflow dispatch not available".into())
302 })?;
303 let input_json = serde_json::to_value(input)?;
304 dispatcher.start_by_name(workflow_name, input_json).await
305 }
306}
307
308impl EnvAccess for MutationContext {
309 fn env_provider(&self) -> &dyn EnvProvider {
310 self.env_provider.as_ref()
311 }
312}
313
314pub struct ActionContext {
316 pub auth: AuthContext,
318 pub request: RequestMetadata,
320 db_pool: sqlx::PgPool,
322 http_client: reqwest::Client,
324 job_dispatch: Option<Arc<dyn JobDispatch>>,
326 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
328 env_provider: Arc<dyn EnvProvider>,
330}
331
332impl ActionContext {
333 pub fn new(
335 db_pool: sqlx::PgPool,
336 auth: AuthContext,
337 request: RequestMetadata,
338 http_client: reqwest::Client,
339 ) -> Self {
340 Self {
341 auth,
342 request,
343 db_pool,
344 http_client,
345 job_dispatch: None,
346 workflow_dispatch: None,
347 env_provider: Arc::new(RealEnvProvider::new()),
348 }
349 }
350
351 pub fn with_dispatch(
353 db_pool: sqlx::PgPool,
354 auth: AuthContext,
355 request: RequestMetadata,
356 http_client: reqwest::Client,
357 job_dispatch: Option<Arc<dyn JobDispatch>>,
358 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
359 ) -> Self {
360 Self {
361 auth,
362 request,
363 db_pool,
364 http_client,
365 job_dispatch,
366 workflow_dispatch,
367 env_provider: Arc::new(RealEnvProvider::new()),
368 }
369 }
370
371 pub fn with_env(
373 db_pool: sqlx::PgPool,
374 auth: AuthContext,
375 request: RequestMetadata,
376 http_client: reqwest::Client,
377 job_dispatch: Option<Arc<dyn JobDispatch>>,
378 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
379 env_provider: Arc<dyn EnvProvider>,
380 ) -> Self {
381 Self {
382 auth,
383 request,
384 db_pool,
385 http_client,
386 job_dispatch,
387 workflow_dispatch,
388 env_provider,
389 }
390 }
391
392 pub fn db(&self) -> &sqlx::PgPool {
394 &self.db_pool
395 }
396
397 pub fn http(&self) -> &reqwest::Client {
399 &self.http_client
400 }
401
402 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
404 self.auth.require_user_id()
405 }
406
407 pub async fn dispatch_job<T: serde::Serialize>(
416 &self,
417 job_type: &str,
418 args: T,
419 ) -> crate::error::Result<Uuid> {
420 let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
421 crate::error::ForgeError::Internal("Job dispatch not available".into())
422 })?;
423 let args_json = serde_json::to_value(args)?;
424 dispatcher.dispatch_by_name(job_type, args_json).await
425 }
426
427 pub async fn start_workflow<T: serde::Serialize>(
436 &self,
437 workflow_name: &str,
438 input: T,
439 ) -> crate::error::Result<Uuid> {
440 let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
441 crate::error::ForgeError::Internal("Workflow dispatch not available".into())
442 })?;
443 let input_json = serde_json::to_value(input)?;
444 dispatcher.start_by_name(workflow_name, input_json).await
445 }
446}
447
448impl EnvAccess for ActionContext {
449 fn env_provider(&self) -> &dyn EnvProvider {
450 self.env_provider.as_ref()
451 }
452}
453
454#[cfg(test)]
455mod tests {
456 use super::*;
457
458 #[test]
459 fn test_auth_context_unauthenticated() {
460 let ctx = AuthContext::unauthenticated();
461 assert!(!ctx.is_authenticated());
462 assert!(ctx.user_id().is_none());
463 assert!(ctx.require_user_id().is_err());
464 }
465
466 #[test]
467 fn test_auth_context_authenticated() {
468 let user_id = Uuid::new_v4();
469 let ctx = AuthContext::authenticated(
470 user_id,
471 vec!["admin".to_string(), "user".to_string()],
472 HashMap::new(),
473 );
474
475 assert!(ctx.is_authenticated());
476 assert_eq!(ctx.user_id(), Some(user_id));
477 assert!(ctx.require_user_id().is_ok());
478 assert!(ctx.has_role("admin"));
479 assert!(ctx.has_role("user"));
480 assert!(!ctx.has_role("superadmin"));
481 assert!(ctx.require_role("admin").is_ok());
482 assert!(ctx.require_role("superadmin").is_err());
483 }
484
485 #[test]
486 fn test_auth_context_with_claims() {
487 let mut claims = HashMap::new();
488 claims.insert("org_id".to_string(), serde_json::json!("org-123"));
489
490 let ctx = AuthContext::authenticated(Uuid::new_v4(), vec![], claims);
491
492 assert_eq!(ctx.claim("org_id"), Some(&serde_json::json!("org-123")));
493 assert!(ctx.claim("nonexistent").is_none());
494 }
495
496 #[test]
497 fn test_request_metadata() {
498 let meta = RequestMetadata::new();
499 assert!(!meta.trace_id.is_empty());
500 assert!(meta.client_ip.is_none());
501
502 let meta2 = RequestMetadata::with_trace_id("trace-123".to_string());
503 assert_eq!(meta2.trace_id, "trace-123");
504 }
505}