mockforge_ui/
routes.rs

1//! Route definitions for the admin UI
2
3use axum::{
4    routing::{delete, get, post},
5    Router,
6};
7use tower_http::{compression::CompressionLayer, cors::CorsLayer};
8
9use crate::audit::init_global_audit_store;
10use crate::auth::init_global_user_store;
11use crate::handlers::analytics::AnalyticsState;
12use crate::handlers::AdminState;
13use crate::handlers::*;
14use crate::rbac::rbac_middleware;
15use crate::time_travel_handlers;
16use axum::middleware::from_fn;
17use mockforge_core::{get_global_logger, init_global_logger};
18
19/// Create the admin router with static assets and optional API endpoints
20///
21/// # Arguments
22/// * `http_server_addr` - HTTP server address
23/// * `ws_server_addr` - WebSocket server address
24/// * `grpc_server_addr` - gRPC server address
25/// * `graphql_server_addr` - GraphQL server address
26/// * `api_enabled` - Whether API endpoints are enabled
27/// * `admin_port` - Admin server port
28/// * `prometheus_url` - Prometheus metrics URL
29/// * `chaos_api_state` - Optional chaos API state for hot-reload support
30/// * `latency_injector` - Optional latency injector for hot-reload support
31/// * `mockai` - Optional MockAI instance for hot-reload support
32/// * `continuum_config` - Optional Reality Continuum configuration
33/// * `virtual_clock` - Optional virtual clock for time-based progression
34pub fn create_admin_router(
35    http_server_addr: Option<std::net::SocketAddr>,
36    ws_server_addr: Option<std::net::SocketAddr>,
37    grpc_server_addr: Option<std::net::SocketAddr>,
38    graphql_server_addr: Option<std::net::SocketAddr>,
39    api_enabled: bool,
40    admin_port: u16,
41    prometheus_url: String,
42    chaos_api_state: Option<std::sync::Arc<mockforge_chaos::api::ChaosApiState>>,
43    latency_injector: Option<
44        std::sync::Arc<tokio::sync::RwLock<mockforge_core::latency::LatencyInjector>>,
45    >,
46    mockai: Option<
47        std::sync::Arc<tokio::sync::RwLock<mockforge_core::intelligent_behavior::MockAI>>,
48    >,
49    continuum_config: Option<mockforge_core::ContinuumConfig>,
50    virtual_clock: Option<std::sync::Arc<mockforge_core::VirtualClock>>,
51) -> Router {
52    // Initialize global logger if not already initialized
53    let _logger = get_global_logger().unwrap_or_else(|| init_global_logger(1000));
54
55    // Initialize audit log store (keep last 10000 audit entries)
56    let _audit_store = init_global_audit_store(10000);
57
58    // Initialize user store for authentication
59    let _user_store = init_global_user_store();
60
61    let state = AdminState::new(
62        http_server_addr,
63        ws_server_addr,
64        grpc_server_addr,
65        graphql_server_addr,
66        api_enabled,
67        admin_port,
68        chaos_api_state,
69        latency_injector,
70        mockai,
71        continuum_config,
72        virtual_clock,
73    );
74
75    // Start system monitoring background task to poll CPU, memory, and thread metrics
76    let state_clone = state.clone();
77    tokio::spawn(async move {
78        state_clone.start_system_monitoring().await;
79    });
80    let mut router = Router::new()
81        // Public routes (no authentication required)
82        .route("/", get(serve_admin_html))
83        .route("/assets/index.css", get(serve_admin_css))
84        .route("/assets/index.js", get(serve_admin_js))
85        .route("/assets/{filename}", get(serve_vendor_asset))
86        .route("/api-docs", get(serve_api_docs))
87        .route("/mockforge-icon.png", get(serve_icon))
88        .route("/mockforge-icon-32.png", get(serve_icon_32))
89        .route("/mockforge-icon-48.png", get(serve_icon_48))
90        .route("/mockforge-logo.png", get(serve_logo))
91        .route("/mockforge-logo-40.png", get(serve_logo_40))
92        .route("/mockforge-logo-80.png", get(serve_logo_80))
93        .route("/manifest.json", get(serve_manifest))
94        .route("/sw.js", get(serve_service_worker))
95        // Authentication endpoints (public)
96        .route("/__mockforge/auth/login", post(crate::auth::login))
97        .route("/__mockforge/auth/refresh", post(crate::auth::refresh_token))
98        .route("/__mockforge/auth/logout", post(crate::auth::logout))
99        .route("/__mockforge/health", get(get_health));
100
101    // Protected routes (require authentication and RBAC)
102    router = router
103        .route("/__mockforge/dashboard", get(get_dashboard))
104        .route("/_mf", get(get_dashboard))  // Short alias for dashboard
105        .route("/admin/server-info", get(get_server_info))
106        .route("/__mockforge/server-info", get(get_server_info))
107        .route("/__mockforge/routes", get(get_routes))
108        .route("/__mockforge/logs", get(get_logs))
109        .route("/__mockforge/logs/sse", get(logs_sse))
110        .route("/__mockforge/metrics", get(get_metrics))
111        .route("/__mockforge/api/reality/trace/{request_id}", get(get_reality_trace))
112        .route("/__mockforge/api/reality/response-trace/{request_id}", get(get_response_trace))
113        .route("/__mockforge/config", get(get_config))
114        .route("/__mockforge/config/latency", post(update_latency))
115        .route("/__mockforge/config/faults", post(update_faults))
116        .route("/__mockforge/config/proxy", post(update_proxy))
117        .route("/__mockforge/config/traffic-shaping", post(update_traffic_shaping))
118        .route("/__mockforge/logs", delete(clear_logs))
119        .route("/__mockforge/restart", post(restart_servers))
120        .route("/__mockforge/restart/status", get(get_restart_status))
121        .route("/__mockforge/fixtures", get(get_fixtures))
122        .route("/__mockforge/fixtures/{id}", delete(delete_fixture))
123        .route("/__mockforge/fixtures/bulk", delete(delete_fixtures_bulk))
124        .route("/__mockforge/audit/logs", get(get_audit_logs))
125        .route("/__mockforge/audit/stats", get(get_audit_stats))
126        .route("/__mockforge/fixtures/{id}/download", get(download_fixture))
127        .route("/__mockforge/fixtures/{id}/rename", post(rename_fixture))
128        .route("/__mockforge/fixtures/{id}/move", post(move_fixture))
129        // Import routes
130        .route("/__mockforge/import/postman", post(import_postman))
131        .route("/__mockforge/import/insomnia", post(import_insomnia))
132        .route("/__mockforge/import/curl", post(import_curl))
133        .route("/__mockforge/import/preview", post(preview_import))
134        .route("/__mockforge/import/history", get(get_import_history))
135        .route("/__mockforge/import/history/clear", post(clear_import_history))
136        // Plugin management routes
137        .route("/__mockforge/plugins", get(get_plugins))
138        .route("/__mockforge/plugins/status", get(get_plugin_status))
139        .route("/__mockforge/plugins/{id}", get(get_plugin_details))
140        .route("/__mockforge/plugins/{id}", delete(delete_plugin))
141        .route("/__mockforge/plugins/reload", post(reload_plugin))
142        // Workspace management routes (moved to workspace router with WorkspaceState)
143        // These routes are now handled by the workspace router below
144        // Environment management routes
145        .route("/__mockforge/workspaces/{workspace_id}/environments", get(get_environments))
146        .route("/__mockforge/workspaces/{workspace_id}/environments", post(create_environment))
147        .route("/__mockforge/workspaces/{workspace_id}/environments/order", axum::routing::put(update_environments_order))
148        .route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}", axum::routing::put(update_environment))
149        .route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}", delete(delete_environment))
150        .route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/activate", post(set_active_environment))
151        .route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/variables", get(get_environment_variables))
152        .route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/variables", post(set_environment_variable))
153        .route("/__mockforge/workspaces/{workspace_id}/environments/{environment_id}/variables/{variable_name}", delete(remove_environment_variable))
154        // Chain management routes - proxy to main HTTP server
155        .route("/__mockforge/chains", get(proxy_chains_list))
156        .route("/__mockforge/chains", post(proxy_chains_create))
157        .route("/__mockforge/chains/{id}", get(proxy_chain_get))
158        .route("/__mockforge/chains/{id}", axum::routing::put(proxy_chain_update))
159        .route("/__mockforge/chains/{id}", delete(proxy_chain_delete))
160        .route("/__mockforge/chains/{id}/execute", post(proxy_chain_execute))
161        .route("/__mockforge/chains/{id}/validate", post(proxy_chain_validate))
162        .route("/__mockforge/chains/{id}/history", get(proxy_chain_history))
163        // Graph visualization routes
164        .route("/__mockforge/graph", get(get_graph))
165        .route("/__mockforge/graph/sse", get(graph_sse))
166        // Validation configuration routes
167        .route("/__mockforge/validation", get(get_validation))
168        .route("/__mockforge/validation", post(update_validation))
169        // Migration pipeline routes
170        .route("/__mockforge/migration/routes", get(migration::get_migration_routes))
171        .route("/__mockforge/migration/routes/{pattern}/toggle", post(migration::toggle_route_migration))
172        .route("/__mockforge/migration/routes/{pattern}", axum::routing::put(migration::set_route_migration_mode))
173        .route("/__mockforge/migration/groups/{group}/toggle", post(migration::toggle_group_migration))
174        .route("/__mockforge/migration/groups/{group}", axum::routing::put(migration::set_group_migration_mode))
175        .route("/__mockforge/migration/groups", get(migration::get_migration_groups))
176        .route("/__mockforge/migration/status", get(migration::get_migration_status))
177        // Environment variables routes
178        .route("/__mockforge/env", get(get_env_vars))
179        .route("/__mockforge/env", post(update_env_var))
180        // File management routes
181        .route("/__mockforge/files/content", post(get_file_content))
182        .route("/__mockforge/files/save", post(save_file_content))
183        // Smoke test routes
184        .route("/__mockforge/smoke", get(get_smoke_tests))
185        .route("/__mockforge/smoke/run", get(run_smoke_tests_endpoint))
186        // Time travel / temporal testing routes
187        .route("/__mockforge/time-travel/status", get(time_travel_handlers::get_time_travel_status))
188        .route("/__mockforge/time-travel/enable", post(time_travel_handlers::enable_time_travel))
189        .route("/__mockforge/time-travel/disable", post(time_travel_handlers::disable_time_travel))
190        .route("/__mockforge/time-travel/advance", post(time_travel_handlers::advance_time))
191        .route("/__mockforge/time-travel/set", post(time_travel_handlers::set_time))
192        .route("/__mockforge/time-travel/scale", post(time_travel_handlers::set_time_scale))
193        .route("/__mockforge/time-travel/reset", post(time_travel_handlers::reset_time_travel))
194        .route("/__mockforge/time-travel/schedule", post(time_travel_handlers::schedule_response))
195        .route("/__mockforge/time-travel/scheduled", get(time_travel_handlers::list_scheduled_responses))
196        .route("/__mockforge/time-travel/scheduled/{id}", delete(time_travel_handlers::cancel_scheduled_response))
197        .route("/__mockforge/time-travel/scheduled/clear", post(time_travel_handlers::clear_scheduled_responses))
198        .route("/__mockforge/time-travel/scenario/save", post(time_travel_handlers::save_scenario))
199        .route("/__mockforge/time-travel/scenario/load", post(time_travel_handlers::load_scenario))
200        // Cron job management routes
201        .route("/__mockforge/time-travel/cron", get(time_travel_handlers::list_cron_jobs))
202        .route("/__mockforge/time-travel/cron", post(time_travel_handlers::create_cron_job))
203        .route("/__mockforge/time-travel/cron/{id}", get(time_travel_handlers::get_cron_job))
204        .route("/__mockforge/time-travel/cron/{id}", delete(time_travel_handlers::delete_cron_job))
205        .route("/__mockforge/time-travel/cron/{id}/enable", post(time_travel_handlers::set_cron_job_enabled))
206        // Mutation rule management routes
207        .route("/__mockforge/time-travel/mutations", get(time_travel_handlers::list_mutation_rules))
208        .route("/__mockforge/time-travel/mutations", post(time_travel_handlers::create_mutation_rule))
209        .route("/__mockforge/time-travel/mutations/{id}", get(time_travel_handlers::get_mutation_rule))
210        .route("/__mockforge/time-travel/mutations/{id}", delete(time_travel_handlers::delete_mutation_rule))
211        .route("/__mockforge/time-travel/mutations/{id}/enable", post(time_travel_handlers::set_mutation_rule_enabled))
212        // Verification routes
213        .route("/__mockforge/verification/verify", post(verification::verify))
214        .route("/__mockforge/verification/count", post(verification::count))
215        .route("/__mockforge/verification/sequence", post(verification::verify_sequence_handler))
216        .route("/__mockforge/verification/never", post(verification::verify_never_handler))
217        .route("/__mockforge/verification/at-least", post(verification::verify_at_least_handler))
218        // Reality Slider routes
219        .route("/__mockforge/reality/level", get(get_reality_level))
220        .route("/__mockforge/reality/level", axum::routing::put(set_reality_level))
221        .route("/__mockforge/reality/presets", get(list_reality_presets))
222        .route("/__mockforge/reality/presets/import", post(import_reality_preset))
223        .route("/__mockforge/reality/presets/export", post(export_reality_preset))
224        // Reality Continuum routes
225        .route("/__mockforge/continuum/ratio", get(get_continuum_ratio))
226        .route("/__mockforge/continuum/ratio", axum::routing::put(set_continuum_ratio))
227        .route("/__mockforge/continuum/schedule", get(get_continuum_schedule))
228        .route("/__mockforge/continuum/schedule", axum::routing::put(set_continuum_schedule))
229        .route("/__mockforge/continuum/advance", post(advance_continuum_ratio))
230        .route("/__mockforge/continuum/enabled", axum::routing::put(set_continuum_enabled))
231        .route("/__mockforge/continuum/overrides", get(get_continuum_overrides))
232        .route("/__mockforge/continuum/overrides", axum::routing::delete(clear_continuum_overrides))
233        // Contract diff routes
234        .route("/__mockforge/contract-diff/upload", post(contract_diff::upload_request))
235        .route("/__mockforge/contract-diff/submit", post(contract_diff::submit_request))
236        .route("/__mockforge/contract-diff/captures", get(contract_diff::get_captured_requests))
237        .route("/__mockforge/contract-diff/captures/{id}", get(contract_diff::get_captured_request))
238        .route("/__mockforge/contract-diff/captures/{id}/analyze", post(contract_diff::analyze_captured_request))
239        .route("/__mockforge/contract-diff/captures/{id}/patch", post(contract_diff::generate_patch_file))
240        .route("/__mockforge/contract-diff/statistics", get(contract_diff::get_capture_statistics))
241        // Playground routes
242        .route("/__mockforge/playground/endpoints", get(playground::list_playground_endpoints))
243        .route("/__mockforge/playground/execute", post(playground::execute_rest_request))
244        .route("/__mockforge/playground/graphql", post(playground::execute_graphql_query))
245        .route("/__mockforge/playground/graphql/introspect", get(playground::graphql_introspect))
246        .route("/__mockforge/playground/history", get(playground::get_request_history))
247        .route("/__mockforge/playground/history/{id}/replay", post(playground::replay_request))
248        .route("/__mockforge/playground/snippets", post(playground::generate_code_snippet))
249        // Voice + LLM Interface routes
250        .route("/api/v2/voice/process", post(voice::process_voice_command))
251        .route("/__mockforge/voice/process", post(voice::process_voice_command))
252        .route("/api/v2/voice/transpile-hook", post(voice::transpile_hook))
253        .route("/__mockforge/voice/transpile-hook", post(voice::transpile_hook))
254        .route(
255            "/api/v2/voice/create-workspace-scenario",
256            post(voice::create_workspace_scenario),
257        )
258        .route(
259            "/__mockforge/voice/create-workspace-scenario",
260            post(voice::create_workspace_scenario),
261        )
262        .route(
263            "/api/v2/voice/create-workspace-preview",
264            post(voice::create_workspace_preview),
265        )
266        .route(
267            "/__mockforge/voice/create-workspace-preview",
268            post(voice::create_workspace_preview),
269        )
270        // create-workspace-confirm route moved to workspace router with WorkspaceState
271        // AI Studio routes
272        .route("/api/v1/ai-studio/chat", post(ai_studio::chat))
273        .route("/__mockforge/ai-studio/chat", post(ai_studio::chat))
274        .route("/api/v1/ai-studio/generate-mock", post(ai_studio::generate_mock))
275        .route("/__mockforge/ai-studio/generate-mock", post(ai_studio::generate_mock))
276        .route("/api/v1/ai-studio/debug-test", post(ai_studio::debug_test))
277        .route("/__mockforge/ai-studio/debug-test", post(ai_studio::debug_test))
278        .route("/api/v1/ai-studio/generate-persona", post(ai_studio::generate_persona))
279        .route("/__mockforge/ai-studio/generate-persona", post(ai_studio::generate_persona))
280        .route("/api/v1/ai-studio/freeze", post(ai_studio::freeze_artifact))
281        .route("/__mockforge/ai-studio/freeze", post(ai_studio::freeze_artifact))
282        .route("/api/v1/ai-studio/usage", get(ai_studio::get_usage))
283        .route("/__mockforge/ai-studio/usage", get(ai_studio::get_usage))
284        // Failure analysis routes
285        .route("/api/v2/failures/analyze", post(failure_analysis::analyze_failure))
286        .route("/api/v2/failures/{request_id}", get(failure_analysis::get_failure_analysis))
287        .route("/api/v2/failures/recent", get(failure_analysis::list_recent_failures))
288        .route("/__mockforge/failures/analyze", post(failure_analysis::analyze_failure))
289        .route("/__mockforge/failures/{request_id}", get(failure_analysis::get_failure_analysis))
290        .route("/__mockforge/failures/recent", get(failure_analysis::list_recent_failures))
291        // Community portal routes
292        .route("/__mockforge/community/showcase/projects", get(community::get_showcase_projects))
293        .route("/__mockforge/community/showcase/projects/{id}", get(community::get_showcase_project))
294        .route("/__mockforge/community/showcase/categories", get(community::get_showcase_categories))
295        .route("/__mockforge/community/showcase/stories", get(community::get_success_stories))
296        .route("/__mockforge/community/showcase/submit", post(community::submit_showcase_project))
297        .route("/__mockforge/community/learning/resources", get(community::get_learning_resources))
298        .route("/__mockforge/community/learning/resources/{id}", get(community::get_learning_resource))
299        .route("/__mockforge/community/learning/categories", get(community::get_learning_categories))
300        // Behavioral cloning / flow management routes
301        .route("/__mockforge/flows", get(behavioral_cloning::get_flows))
302        .route("/__mockforge/flows/{id}", get(behavioral_cloning::get_flow))
303        .route("/__mockforge/flows/{id}/tag", axum::routing::put(behavioral_cloning::tag_flow))
304        .route("/__mockforge/flows/{id}/compile", post(behavioral_cloning::compile_flow))
305        .route("/__mockforge/scenarios", get(behavioral_cloning::get_scenarios))
306        .route("/__mockforge/scenarios/{id}", get(behavioral_cloning::get_scenario))
307        .route("/__mockforge/scenarios/{id}/export", get(behavioral_cloning::export_scenario))
308        // Health check endpoints for Kubernetes probes
309        .route("/health/live", get(health::liveness_probe))
310        .route("/health/ready", get(health::readiness_probe))
311        .route("/health/startup", get(health::startup_probe))
312        .route("/health", get(health::deep_health_check))
313        // Kubernetes-style health endpoint aliases
314        .route("/healthz", get(health::deep_health_check))
315        .route("/readyz", get(health::readiness_probe))
316        .route("/livez", get(health::liveness_probe))
317        .route("/startupz", get(health::startup_probe));
318
319    // Analytics routes with Prometheus integration
320    let analytics_state = AnalyticsState::new(prometheus_url);
321
322    let analytics_router = Router::new()
323        .route("/__mockforge/analytics/summary", get(analytics::get_summary))
324        .route("/__mockforge/analytics/requests", get(analytics::get_requests))
325        .route("/__mockforge/analytics/endpoints", get(analytics::get_endpoints))
326        .route("/__mockforge/analytics/websocket", get(analytics::get_websocket))
327        .route("/__mockforge/analytics/smtp", get(analytics::get_smtp))
328        .route("/__mockforge/analytics/system", get(analytics::get_system))
329        .with_state(analytics_state);
330
331    router = router.merge(analytics_router);
332
333    // Add workspace router with WorkspaceState
334    {
335        use crate::handlers::workspaces::WorkspaceState;
336        use mockforge_core::multi_tenant::{MultiTenantConfig, MultiTenantWorkspaceRegistry};
337        use std::sync::Arc;
338
339        // Create workspace registry
340        let mt_config = MultiTenantConfig {
341            enabled: true,
342            default_workspace: "default".to_string(),
343            ..Default::default()
344        };
345        let registry = MultiTenantWorkspaceRegistry::new(mt_config);
346        let workspace_state = WorkspaceState::new(Arc::new(tokio::sync::RwLock::new(registry)));
347
348        // Create workspace router with state
349        use crate::handlers::workspaces;
350        let workspace_router = Router::new()
351            .route("/__mockforge/workspaces", get(workspaces::list_workspaces))
352            .route("/__mockforge/workspaces", post(workspaces::create_workspace))
353            .route("/__mockforge/workspaces/{workspace_id}", get(workspaces::get_workspace))
354            .route(
355                "/__mockforge/workspaces/{workspace_id}",
356                axum::routing::put(workspaces::update_workspace),
357            )
358            .route("/__mockforge/workspaces/{workspace_id}", delete(workspaces::delete_workspace))
359            // Note: set_active_workspace handler not yet implemented
360            // .route(
361            //     "/__mockforge/workspaces/{workspace_id}/activate",
362            //     post(workspaces::set_active_workspace),
363            // )
364            .route("/api/v2/voice/create-workspace-confirm", post(voice::create_workspace_confirm))
365            .route(
366                "/__mockforge/voice/create-workspace-confirm",
367                post(voice::create_workspace_confirm),
368            )
369            .with_state(workspace_state);
370
371        router = router.merge(workspace_router);
372        tracing::info!("Workspace router mounted with WorkspaceState");
373    }
374
375    // Add UI Builder router
376    // This provides a low-code visual interface for creating mock endpoints
377    {
378        use mockforge_http::{create_ui_builder_router, UIBuilderState};
379
380        // Load server config for UI Builder
381        // For now, create a default config. In production, this should be loaded from the actual config.
382        // Use the re-exported ServerConfig from mockforge_core root to match UIBuilderState's import
383        let server_config = mockforge_core::ServerConfig::default();
384        let ui_builder_state = UIBuilderState::new(server_config);
385        let ui_builder_router = create_ui_builder_router(ui_builder_state);
386
387        // Nest the UI builder router with its own state
388        router = router.nest_service("/__mockforge/ui-builder", ui_builder_router);
389        tracing::info!("UI Builder mounted at /__mockforge/ui-builder");
390    }
391
392    // SPA fallback: serve index.html for any unmatched routes to support client-side routing
393    // IMPORTANT: This must be AFTER all API routes
394    router = router.route("/{*path}", get(serve_admin_html));
395
396    // Apply RBAC middleware to protected routes
397    // Note: The middleware will check authentication and permissions for all routes
398    // Public routes (auth endpoints, static assets) should be handled gracefully
399    router = router.layer(from_fn(rbac_middleware));
400
401    router
402        .layer(CompressionLayer::new())
403        .layer(CorsLayer::permissive())
404        .with_state(state)
405}
406
407#[cfg(test)]
408mod tests {
409    use super::*;
410
411    #[tokio::test]
412    async fn test_create_admin_router() {
413        let http_addr: std::net::SocketAddr = "127.0.0.1:3000".parse().unwrap();
414        let router = create_admin_router(
415            Some(http_addr),
416            None,
417            None,
418            None,
419            true,
420            8080,
421            "http://localhost:9090".to_string(),
422            None,
423            None,
424            None,
425            None,
426            None,
427        );
428
429        // Router should be created successfully
430        let _ = router;
431    }
432
433    #[tokio::test]
434    async fn test_create_admin_router_no_servers() {
435        let router = create_admin_router(
436            None,
437            None,
438            None,
439            None,
440            false,
441            8080,
442            "http://localhost:9090".to_string(),
443            None,
444            None,
445            None,
446            None,
447            None,
448        );
449
450        // Router should still work without server addresses
451        let _ = router;
452    }
453}