mockforge_core/multi_tenant/
middleware.rs1use super::{MultiTenantWorkspaceRegistry, TenantWorkspace};
7use crate::{Error, Result};
8use std::sync::Arc;
9
10#[derive(Debug, Clone)]
12pub struct WorkspaceContext {
13 pub workspace_id: String,
15 pub original_path: String,
17 pub stripped_path: String,
19 pub workspace: TenantWorkspace,
21}
22
23#[derive(Debug, Clone)]
25pub struct WorkspaceRouter {
26 registry: Arc<MultiTenantWorkspaceRegistry>,
28}
29
30impl WorkspaceRouter {
31 pub fn new(registry: Arc<MultiTenantWorkspaceRegistry>) -> Self {
33 Self { registry }
34 }
35
36 pub fn extract_workspace_context(&self, path: &str) -> Result<WorkspaceContext> {
38 let config = self.registry.config();
39
40 if !config.enabled {
42 let workspace = self.registry.get_default_workspace()?;
43 return Ok(WorkspaceContext {
44 workspace_id: config.default_workspace.clone(),
45 original_path: path.to_string(),
46 stripped_path: path.to_string(),
47 workspace,
48 });
49 }
50
51 if let Some(workspace_id) = self.registry.extract_workspace_id_from_path(path) {
53 let workspace = self.registry.get_workspace(&workspace_id)?;
55
56 if !workspace.enabled {
57 return Err(Error::generic(format!("Workspace '{}' is disabled", workspace_id)));
58 }
59
60 let stripped_path = self.registry.strip_workspace_prefix(path, &workspace_id);
61
62 Ok(WorkspaceContext {
63 workspace_id: workspace_id.clone(),
64 original_path: path.to_string(),
65 stripped_path,
66 workspace,
67 })
68 } else {
69 let workspace = self.registry.get_default_workspace()?;
71
72 Ok(WorkspaceContext {
73 workspace_id: config.default_workspace.clone(),
74 original_path: path.to_string(),
75 stripped_path: path.to_string(),
76 workspace,
77 })
78 }
79 }
80
81 pub fn registry(&self) -> &Arc<MultiTenantWorkspaceRegistry> {
83 &self.registry
84 }
85
86 pub fn get_workspace(&self, workspace_id: &str) -> Result<TenantWorkspace> {
88 self.registry.get_workspace(workspace_id)
89 }
90
91 pub fn is_multi_tenant_enabled(&self) -> bool {
93 self.registry.config().enabled
94 }
95
96 pub fn workspace_prefix(&self) -> &str {
98 &self.registry.config().workspace_prefix
99 }
100}
101
102pub mod axum_middleware {
104 use super::*;
105 use ::axum::http::StatusCode;
106 use ::axum::{
107 extract::Request,
108 middleware::Next,
109 response::{IntoResponse, Response},
110 };
111
112 pub async fn workspace_middleware(
114 router: Arc<WorkspaceRouter>,
115 mut request: Request,
116 next: Next,
117 ) -> Response {
118 let path = request.uri().path();
119
120 let context = match router.extract_workspace_context(path) {
122 Ok(ctx) => ctx,
123 Err(e) => {
124 return (StatusCode::NOT_FOUND, format!("Workspace error: {}", e)).into_response();
125 }
126 };
127
128 request.extensions_mut().insert(context.clone());
130
131 if context.original_path != context.stripped_path {
133 let mut parts = request.uri().clone().into_parts();
134 parts.path_and_query = context.stripped_path.parse().ok().or(parts.path_and_query);
135
136 if let Ok(uri) = ::axum::http::Uri::from_parts(parts) {
137 *request.uri_mut() = uri;
138 }
139 }
140
141 next.run(request).await
143 }
144
145 pub trait WorkspaceContextExt {
147 fn workspace_context(&self) -> Option<&WorkspaceContext>;
149
150 fn workspace_id(&self) -> Option<&str>;
152
153 fn stripped_path(&self) -> Option<&str>;
155 }
156
157 impl WorkspaceContextExt for Request {
158 fn workspace_context(&self) -> Option<&WorkspaceContext> {
159 self.extensions().get::<WorkspaceContext>()
160 }
161
162 fn workspace_id(&self) -> Option<&str> {
163 self.workspace_context().map(|ctx| ctx.workspace_id.as_str())
164 }
165
166 fn stripped_path(&self) -> Option<&str> {
167 self.workspace_context().map(|ctx| ctx.stripped_path.as_str())
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175 use crate::multi_tenant::{MultiTenantConfig, MultiTenantWorkspaceRegistry};
176 use crate::workspace::Workspace;
177
178 fn create_test_router() -> WorkspaceRouter {
179 let config = MultiTenantConfig {
180 enabled: true,
181 ..Default::default()
182 };
183
184 let mut registry = MultiTenantWorkspaceRegistry::new(config);
185
186 let default_ws = Workspace::new("Default".to_string());
188 registry.register_workspace("default".to_string(), default_ws).unwrap();
189
190 let test_ws = Workspace::new("Test Workspace".to_string());
192 registry.register_workspace("test".to_string(), test_ws).unwrap();
193
194 WorkspaceRouter::new(Arc::new(registry))
195 }
196
197 #[test]
198 fn test_extract_workspace_context_with_prefix() {
199 let router = create_test_router();
200
201 let context = router.extract_workspace_context("/workspace/test/api/users").unwrap();
202
203 assert_eq!(context.workspace_id, "test");
204 assert_eq!(context.original_path, "/workspace/test/api/users");
205 assert_eq!(context.stripped_path, "/api/users");
206 assert_eq!(context.workspace.name(), "Test Workspace");
207 }
208
209 #[test]
210 fn test_extract_workspace_context_default() {
211 let router = create_test_router();
212
213 let context = router.extract_workspace_context("/api/users").unwrap();
214
215 assert_eq!(context.workspace_id, "default");
216 assert_eq!(context.original_path, "/api/users");
217 assert_eq!(context.stripped_path, "/api/users");
218 assert_eq!(context.workspace.name(), "Default");
219 }
220
221 #[test]
222 fn test_extract_workspace_context_nonexistent() {
223 let router = create_test_router();
224
225 let result = router.extract_workspace_context("/workspace/nonexistent/api/users");
226
227 assert!(result.is_err());
228 }
229
230 #[test]
231 fn test_multi_tenant_disabled() {
232 let config = MultiTenantConfig {
233 enabled: false,
234 ..Default::default()
235 };
236
237 let mut registry = MultiTenantWorkspaceRegistry::new(config);
238
239 let default_ws = Workspace::new("Default".to_string());
240 registry.register_workspace("default".to_string(), default_ws).unwrap();
241
242 let router = WorkspaceRouter::new(Arc::new(registry));
243
244 let context = router.extract_workspace_context("/workspace/test/api/users").unwrap();
245
246 assert_eq!(context.workspace_id, "default");
248 assert_eq!(context.stripped_path, "/workspace/test/api/users");
249 }
250}