1use std::collections::HashMap;
2use std::sync::Arc;
3
4use uuid::Uuid;
5
6use super::dispatch::{JobDispatch, WorkflowDispatch};
7
8#[derive(Debug, Clone)]
10pub struct AuthContext {
11 user_id: Option<Uuid>,
13 roles: Vec<String>,
15 claims: HashMap<String, serde_json::Value>,
17 authenticated: bool,
19}
20
21impl AuthContext {
22 pub fn unauthenticated() -> Self {
24 Self {
25 user_id: None,
26 roles: Vec::new(),
27 claims: HashMap::new(),
28 authenticated: false,
29 }
30 }
31
32 pub fn authenticated(
34 user_id: Uuid,
35 roles: Vec<String>,
36 claims: HashMap<String, serde_json::Value>,
37 ) -> Self {
38 Self {
39 user_id: Some(user_id),
40 roles,
41 claims,
42 authenticated: true,
43 }
44 }
45
46 pub fn is_authenticated(&self) -> bool {
48 self.authenticated
49 }
50
51 pub fn user_id(&self) -> Option<Uuid> {
53 self.user_id
54 }
55
56 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
58 self.user_id
59 .ok_or_else(|| crate::error::ForgeError::Unauthorized("Authentication required".into()))
60 }
61
62 pub fn has_role(&self, role: &str) -> bool {
64 self.roles.iter().any(|r| r == role)
65 }
66
67 pub fn require_role(&self, role: &str) -> crate::error::Result<()> {
69 if self.has_role(role) {
70 Ok(())
71 } else {
72 Err(crate::error::ForgeError::Forbidden(format!(
73 "Required role '{}' not present",
74 role
75 )))
76 }
77 }
78
79 pub fn claim(&self, key: &str) -> Option<&serde_json::Value> {
81 self.claims.get(key)
82 }
83
84 pub fn roles(&self) -> &[String] {
86 &self.roles
87 }
88}
89
90#[derive(Debug, Clone)]
92pub struct RequestMetadata {
93 pub request_id: Uuid,
95 pub trace_id: String,
97 pub client_ip: Option<String>,
99 pub user_agent: Option<String>,
101 pub timestamp: chrono::DateTime<chrono::Utc>,
103}
104
105impl RequestMetadata {
106 pub fn new() -> Self {
108 Self {
109 request_id: Uuid::new_v4(),
110 trace_id: Uuid::new_v4().to_string(),
111 client_ip: None,
112 user_agent: None,
113 timestamp: chrono::Utc::now(),
114 }
115 }
116
117 pub fn with_trace_id(trace_id: String) -> Self {
119 Self {
120 request_id: Uuid::new_v4(),
121 trace_id,
122 client_ip: None,
123 user_agent: None,
124 timestamp: chrono::Utc::now(),
125 }
126 }
127}
128
129impl Default for RequestMetadata {
130 fn default() -> Self {
131 Self::new()
132 }
133}
134
135pub struct QueryContext {
137 pub auth: AuthContext,
139 pub request: RequestMetadata,
141 db_pool: sqlx::PgPool,
143}
144
145impl QueryContext {
146 pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
148 Self {
149 auth,
150 request,
151 db_pool,
152 }
153 }
154
155 pub fn db(&self) -> &sqlx::PgPool {
157 &self.db_pool
158 }
159
160 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
162 self.auth.require_user_id()
163 }
164}
165
166pub struct MutationContext {
168 pub auth: AuthContext,
170 pub request: RequestMetadata,
172 db_pool: sqlx::PgPool,
174 job_dispatch: Option<Arc<dyn JobDispatch>>,
176 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
178}
179
180impl MutationContext {
181 pub fn new(db_pool: sqlx::PgPool, auth: AuthContext, request: RequestMetadata) -> Self {
183 Self {
184 auth,
185 request,
186 db_pool,
187 job_dispatch: None,
188 workflow_dispatch: None,
189 }
190 }
191
192 pub fn with_dispatch(
194 db_pool: sqlx::PgPool,
195 auth: AuthContext,
196 request: RequestMetadata,
197 job_dispatch: Option<Arc<dyn JobDispatch>>,
198 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
199 ) -> Self {
200 Self {
201 auth,
202 request,
203 db_pool,
204 job_dispatch,
205 workflow_dispatch,
206 }
207 }
208
209 pub fn db(&self) -> &sqlx::PgPool {
211 &self.db_pool
212 }
213
214 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
216 self.auth.require_user_id()
217 }
218
219 pub async fn dispatch_job<T: serde::Serialize>(
228 &self,
229 job_type: &str,
230 args: T,
231 ) -> crate::error::Result<Uuid> {
232 let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
233 crate::error::ForgeError::Internal("Job dispatch not available".into())
234 })?;
235 let args_json = serde_json::to_value(args)?;
236 dispatcher.dispatch_by_name(job_type, args_json).await
237 }
238
239 pub async fn start_workflow<T: serde::Serialize>(
248 &self,
249 workflow_name: &str,
250 input: T,
251 ) -> crate::error::Result<Uuid> {
252 let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
253 crate::error::ForgeError::Internal("Workflow dispatch not available".into())
254 })?;
255 let input_json = serde_json::to_value(input)?;
256 dispatcher.start_by_name(workflow_name, input_json).await
257 }
258}
259
260pub struct ActionContext {
262 pub auth: AuthContext,
264 pub request: RequestMetadata,
266 db_pool: sqlx::PgPool,
268 http_client: reqwest::Client,
270 job_dispatch: Option<Arc<dyn JobDispatch>>,
272 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
274}
275
276impl ActionContext {
277 pub fn new(
279 db_pool: sqlx::PgPool,
280 auth: AuthContext,
281 request: RequestMetadata,
282 http_client: reqwest::Client,
283 ) -> Self {
284 Self {
285 auth,
286 request,
287 db_pool,
288 http_client,
289 job_dispatch: None,
290 workflow_dispatch: None,
291 }
292 }
293
294 pub fn with_dispatch(
296 db_pool: sqlx::PgPool,
297 auth: AuthContext,
298 request: RequestMetadata,
299 http_client: reqwest::Client,
300 job_dispatch: Option<Arc<dyn JobDispatch>>,
301 workflow_dispatch: Option<Arc<dyn WorkflowDispatch>>,
302 ) -> Self {
303 Self {
304 auth,
305 request,
306 db_pool,
307 http_client,
308 job_dispatch,
309 workflow_dispatch,
310 }
311 }
312
313 pub fn db(&self) -> &sqlx::PgPool {
315 &self.db_pool
316 }
317
318 pub fn http(&self) -> &reqwest::Client {
320 &self.http_client
321 }
322
323 pub fn require_user_id(&self) -> crate::error::Result<Uuid> {
325 self.auth.require_user_id()
326 }
327
328 pub async fn dispatch_job<T: serde::Serialize>(
337 &self,
338 job_type: &str,
339 args: T,
340 ) -> crate::error::Result<Uuid> {
341 let dispatcher = self.job_dispatch.as_ref().ok_or_else(|| {
342 crate::error::ForgeError::Internal("Job dispatch not available".into())
343 })?;
344 let args_json = serde_json::to_value(args)?;
345 dispatcher.dispatch_by_name(job_type, args_json).await
346 }
347
348 pub async fn start_workflow<T: serde::Serialize>(
357 &self,
358 workflow_name: &str,
359 input: T,
360 ) -> crate::error::Result<Uuid> {
361 let dispatcher = self.workflow_dispatch.as_ref().ok_or_else(|| {
362 crate::error::ForgeError::Internal("Workflow dispatch not available".into())
363 })?;
364 let input_json = serde_json::to_value(input)?;
365 dispatcher.start_by_name(workflow_name, input_json).await
366 }
367}
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
374 fn test_auth_context_unauthenticated() {
375 let ctx = AuthContext::unauthenticated();
376 assert!(!ctx.is_authenticated());
377 assert!(ctx.user_id().is_none());
378 assert!(ctx.require_user_id().is_err());
379 }
380
381 #[test]
382 fn test_auth_context_authenticated() {
383 let user_id = Uuid::new_v4();
384 let ctx = AuthContext::authenticated(
385 user_id,
386 vec!["admin".to_string(), "user".to_string()],
387 HashMap::new(),
388 );
389
390 assert!(ctx.is_authenticated());
391 assert_eq!(ctx.user_id(), Some(user_id));
392 assert!(ctx.require_user_id().is_ok());
393 assert!(ctx.has_role("admin"));
394 assert!(ctx.has_role("user"));
395 assert!(!ctx.has_role("superadmin"));
396 assert!(ctx.require_role("admin").is_ok());
397 assert!(ctx.require_role("superadmin").is_err());
398 }
399
400 #[test]
401 fn test_auth_context_with_claims() {
402 let mut claims = HashMap::new();
403 claims.insert("org_id".to_string(), serde_json::json!("org-123"));
404
405 let ctx = AuthContext::authenticated(Uuid::new_v4(), vec![], claims);
406
407 assert_eq!(ctx.claim("org_id"), Some(&serde_json::json!("org-123")));
408 assert!(ctx.claim("nonexistent").is_none());
409 }
410
411 #[test]
412 fn test_request_metadata() {
413 let meta = RequestMetadata::new();
414 assert!(!meta.trace_id.is_empty());
415 assert!(meta.client_ip.is_none());
416
417 let meta2 = RequestMetadata::with_trace_id("trace-123".to_string());
418 assert_eq!(meta2.trace_id, "trace-123");
419 }
420}