use axum::{
middleware,
routing::{delete, get, patch, post, put},
Router,
};
use crate::handlers;
use crate::middleware::{auth_middleware, rate_limit_middleware};
use crate::AppState;
pub fn create_router(state: AppState) -> Router<AppState> {
let public_routes = Router::new()
.route("/health", get(handlers::health::health_check))
.route("/health/live", get(handlers::health::liveness_check))
.route("/health/ready", get(handlers::health::readiness_check))
.route("/health/circuits", get(handlers::health::circuit_breaker_status))
.route("/api/v1/plugins/search", post(handlers::plugins::search_plugins))
.route("/api/v1/plugins/{name}", get(handlers::plugins::get_plugin))
.route("/api/v1/plugins/{name}/versions/{version}", get(handlers::plugins::get_version))
.route("/api/v1/plugins/{name}/reviews", get(handlers::reviews::get_reviews))
.route("/api/v1/plugins/{name}/badges", get(handlers::admin::get_plugin_badges))
.route("/api/v1/plugins/{name}/security", get(handlers::plugins::get_plugin_security))
.route("/api/v1/stats", get(handlers::stats::get_stats))
.route("/api/v1/auth/register", post(handlers::auth::register))
.route("/api/v1/auth/login", post(handlers::auth::login))
.route("/api/v1/auth/token/refresh", post(handlers::auth::refresh_token))
.route(
"/api/v1/auth/password/reset-request",
post(handlers::auth::request_password_reset),
)
.route("/api/v1/auth/password/reset", post(handlers::auth::confirm_password_reset))
.route("/api/v1/auth/verify-email", get(handlers::verification::verify_email))
.route("/api/v1/faq", get(handlers::faq::get_faq))
.route("/api/v1/legal/terms", get(handlers::legal::get_terms))
.route("/api/v1/legal/privacy", get(handlers::legal::get_privacy))
.route("/api/v1/legal/dpa", get(handlers::legal::get_dpa))
.route("/api/v1/status", get(handlers::status::get_status))
.route("/api/v1/support/contact", post(handlers::support::submit_contact))
.route("/api/v1/waitlist/subscribe", post(handlers::waitlist::subscribe))
.route("/api/v1/waitlist/unsubscribe", get(handlers::waitlist::unsubscribe))
.route("/api/v1/marketplace/scenarios/search", post(handlers::scenarios::search_scenarios))
.route("/api/v1/marketplace/scenarios/{name}", get(handlers::scenarios::get_scenario))
.route("/api/v1/marketplace/scenarios/{name}/versions/{version}", get(handlers::scenarios::get_scenario_version))
.route("/api/v1/marketplace/scenarios/{name}/reviews", get(handlers::scenario_reviews::get_scenario_reviews))
.route("/api/v1/marketplace/templates/search", post(handlers::templates::search_templates))
.route("/api/v1/marketplace/templates/{name}/{version}", get(handlers::templates::get_template))
.route("/api/v1/marketplace/templates/{name}/{version}/reviews", get(handlers::template_reviews::get_template_reviews))
.route_layer(middleware::from_fn(rate_limit_middleware));
let auth_routes = Router::new()
.route("/api/v1/plugins/publish", post(handlers::plugins::publish_plugin))
.route(
"/api/v1/plugins/{name}/versions/{version}/yank",
delete(handlers::plugins::yank_version),
)
.route("/api/v1/plugins/{name}/reviews", post(handlers::reviews::submit_review))
.route(
"/api/v1/plugins/{name}/reviews/{review_id}/vote",
post(handlers::reviews::vote_review),
)
.route("/api/v1/auth/verify", get(handlers::auth::verify_token))
.route("/api/v1/auth/me", get(handlers::auth::me))
.route(
"/api/v1/users/me/public-keys",
get(handlers::public_keys::list_my_public_keys),
)
.route(
"/api/v1/users/me/public-keys",
post(handlers::public_keys::create_my_public_key),
)
.route(
"/api/v1/users/me/public-keys/{id}",
delete(handlers::public_keys::revoke_my_public_key),
)
.route("/api/v1/auth/2fa/setup", get(handlers::two_factor::setup_2fa))
.route("/api/v1/auth/2fa/verify-setup", post(handlers::two_factor::verify_2fa_setup_with_secret))
.route("/api/v1/auth/2fa/disable", post(handlers::two_factor::disable_2fa))
.route("/api/v1/auth/2fa/status", get(handlers::two_factor::get_2fa_status))
.route("/api/v1/organizations", get(handlers::organizations::list_organizations))
.route("/api/v1/organizations", post(handlers::organizations::create_organization))
.route("/api/v1/organizations/{org_id}", get(handlers::organizations::get_organization))
.route("/api/v1/organizations/{org_id}", patch(handlers::organizations::update_organization))
.route("/api/v1/organizations/{org_id}", delete(handlers::organizations::delete_organization))
.route("/api/v1/organizations/{org_id}/members", get(handlers::organizations::get_organization_members))
.route("/api/v1/organizations/{org_id}/members", post(handlers::organizations::add_organization_member))
.route("/api/v1/organizations/{org_id}/members/{user_id}", patch(handlers::organizations::update_organization_member_role))
.route("/api/v1/organizations/{org_id}/members/{user_id}", delete(handlers::organizations::remove_organization_member))
.route("/api/v1/organizations/slug/{slug}", get(handlers::organizations::get_organization_by_slug))
.route("/api/v1/organizations/{org_id}/quota", get(handlers::organizations::get_organization_quota))
.route("/api/v1/organizations/{org_id}/quota", put(handlers::organizations::set_organization_quota))
.route("/api/v1/organizations/{org_id}/invitations", post(handlers::organizations::create_invitation))
.route("/api/v1/invitations/{token}", get(handlers::organizations::get_invitation))
.route("/api/v1/invitations/{token}/accept", post(handlers::organizations::accept_invitation))
.route("/api/v1/users/email/{email}", get(handlers::organizations::find_user_by_email_admin))
.route("/api/v1/users/username/{username}", get(handlers::organizations::find_user_by_username_admin))
.route("/api/v1/users/{user_id}/verify", post(handlers::organizations::mark_user_verified_admin))
.route("/api/v1/organizations/{org_id}/settings", get(handlers::organization_settings::get_organization_settings))
.route("/api/v1/organizations/{org_id}/settings", patch(handlers::organization_settings::update_organization_settings))
.route("/api/v1/organizations/{org_id}/settings/ai", get(handlers::organization_settings::get_organization_ai_settings))
.route("/api/v1/organizations/{org_id}/settings/ai", patch(handlers::organization_settings::update_organization_ai_settings))
.route("/api/v1/organizations/{org_id}/usage", get(handlers::organization_settings::get_organization_usage))
.route("/api/v1/organizations/{org_id}/billing", get(handlers::organization_settings::get_organization_billing))
.route("/api/v1/organizations/{org_id}/analytics/pillars", get(handlers::pillar_analytics::get_org_pillar_metrics))
.route("/api/v1/workspaces/{workspace_id}/analytics/pillars", get(handlers::pillar_analytics::get_workspace_pillar_metrics))
.route("/api/v1/analytics/pillars/events", post(handlers::pillar_analytics::record_pillar_event))
.route("/api/v1/sso/config", get(handlers::sso::get_sso_config))
.route("/api/v1/sso/config", post(handlers::sso::create_sso_config))
.route("/api/v1/sso/config", delete(handlers::sso::delete_sso_config))
.route("/api/v1/sso/enable", post(handlers::sso::enable_sso))
.route("/api/v1/sso/disable", post(handlers::sso::disable_sso))
.route("/api/v1/sso/saml/metadata/{org_slug}", get(handlers::sso::get_saml_metadata))
.route("/api/v1/billing/subscription", get(handlers::billing::get_subscription))
.route("/api/v1/billing/checkout", post(handlers::billing::create_checkout))
.route("/api/v1/billing/portal", post(handlers::billing::create_portal_session))
.route("/api/v1/auth/verify-email/resend", post(handlers::verification::resend_verification))
.route("/api/v1/hosted-mocks/specs/upload", post(handlers::hosted_mocks::upload_spec))
.route("/api/v1/hosted-mocks", post(handlers::hosted_mocks::create_deployment))
.route("/api/v1/hosted-mocks", get(handlers::hosted_mocks::list_deployments))
.route("/api/v1/hosted-mocks/{deployment_id}", get(handlers::hosted_mocks::get_deployment))
.route("/api/v1/hosted-mocks/{deployment_id}/status", patch(handlers::hosted_mocks::update_deployment_status))
.route("/api/v1/hosted-mocks/{deployment_id}", delete(handlers::hosted_mocks::delete_deployment))
.route("/api/v1/hosted-mocks/{deployment_id}/redeploy", post(handlers::hosted_mocks::redeploy_deployment))
.route("/api/v1/hosted-mocks/{deployment_id}/set-domain", post(handlers::hosted_mocks::set_domain))
.route("/api/v1/hosted-mocks/{deployment_id}/stop", post(handlers::hosted_mocks::stop_deployment))
.route("/api/v1/hosted-mocks/{deployment_id}/start", post(handlers::hosted_mocks::start_deployment))
.route("/api/v1/hosted-mocks/{deployment_id}/logs", get(handlers::hosted_mocks::get_deployment_logs))
.route("/api/v1/hosted-mocks/{deployment_id}/metrics", get(handlers::hosted_mocks::get_deployment_metrics))
.route("/api/v1/projects", get(handlers::projects::list_projects))
.route("/api/v1/tokens", post(handlers::tokens::create_token))
.route("/api/v1/tokens", get(handlers::tokens::list_tokens))
.route("/api/v1/tokens/{token_id}", delete(handlers::tokens::delete_token))
.route("/api/v1/usage", get(handlers::usage::get_usage))
.route("/api/v1/usage/history", get(handlers::usage::get_usage_history))
.route("/api/v1/usage/ai-tokens", post(handlers::usage::report_ai_tokens))
.route("/api/v1/organizations/{org_id}/audit-logs", get(handlers::audit::get_audit_logs))
.route("/api/v1/gdpr/export", get(handlers::gdpr::export_data))
.route("/api/v1/gdpr/erase", delete(handlers::gdpr::delete_data))
.route("/api/v1/security/suspicious-activities", get(handlers::security::get_suspicious_activities))
.route("/api/v1/security/suspicious-activities/{id}/resolve", post(handlers::security::resolve_suspicious_activity))
.route("/api/v1/settings/byok", get(handlers::settings::get_byok_config))
.route("/api/v1/settings/byok", put(handlers::settings::update_byok_config))
.route("/api/v1/settings/byok", delete(handlers::settings::delete_byok_config))
.route("/api/v1/settings/byok/test", post(handlers::settings::test_byok_connection))
.route("/api/v1/tokens/{token_id}/rotate", post(handlers::token_rotation::rotate_token))
.route("/api/v1/tokens/rotation-status", get(handlers::token_rotation::get_tokens_needing_rotation))
.route("/api/v1/dashboard", get(handlers::cloud_dashboard::get_dashboard))
.route("/api/v1/dashboard/health", get(handlers::cloud_dashboard::get_health))
.route("/api/v1/dashboard/logs", get(handlers::cloud_dashboard::get_logs))
.route("/api/v1/workspaces", get(handlers::cloud_workspaces::list_workspaces))
.route("/api/v1/workspaces", post(handlers::cloud_workspaces::create_workspace))
.route("/api/v1/workspaces/{id}", get(handlers::cloud_workspaces::get_workspace))
.route("/api/v1/workspaces/{id}", patch(handlers::cloud_workspaces::update_workspace))
.route("/api/v1/workspaces/{id}", delete(handlers::cloud_workspaces::delete_workspace))
.route("/api/v1/services", get(handlers::cloud_services::list_services))
.route("/api/v1/services", post(handlers::cloud_services::create_service))
.route("/api/v1/services/{id}", get(handlers::cloud_services::get_service))
.route("/api/v1/services/{id}", patch(handlers::cloud_services::update_service))
.route("/api/v1/services/{id}", delete(handlers::cloud_services::delete_service))
.route("/api/v1/fixtures", get(handlers::cloud_fixtures::list_fixtures))
.route("/api/v1/fixtures", post(handlers::cloud_fixtures::create_fixture))
.route("/api/v1/fixtures/{id}", get(handlers::cloud_fixtures::get_fixture))
.route("/api/v1/fixtures/{id}", patch(handlers::cloud_fixtures::update_fixture))
.route("/api/v1/fixtures/{id}", delete(handlers::cloud_fixtures::delete_fixture))
.route("/api/v1/federation", get(handlers::federations::list_federations))
.route("/api/v1/federation", post(handlers::federations::create_federation))
.route("/api/v1/federation/{id}", get(handlers::federations::get_federation))
.route("/api/v1/federation/{id}", patch(handlers::federations::update_federation))
.route("/api/v1/federation/{id}", delete(handlers::federations::delete_federation))
.route("/api/v1/marketplace/scenarios/publish", post(handlers::scenarios::publish_scenario))
.route("/api/v1/marketplace/scenarios/{name}/reviews", post(handlers::scenario_reviews::submit_scenario_review))
.route("/api/v1/marketplace/templates/publish", post(handlers::templates::publish_template))
.route("/api/v1/marketplace/templates/{name}/{version}/reviews", post(handlers::template_reviews::submit_template_review))
.route("/api/v1/marketplace/templates/{name}/{version}/star", post(handlers::templates::toggle_template_star))
.route("/api/v1/marketplace/templates/{name}/{version}/star", get(handlers::templates::get_template_star_state))
.route("/api/v1/organizations/{org_id}/templates", get(handlers::org_templates::list_templates))
.route("/api/v1/organizations/{org_id}/templates", post(handlers::org_templates::create_template))
.route("/api/v1/organizations/{org_id}/templates/{template_id}", get(handlers::org_templates::get_template))
.route("/api/v1/organizations/{org_id}/templates/{template_id}", patch(handlers::org_templates::update_template))
.route("/api/v1/organizations/{org_id}/templates/{template_id}", delete(handlers::org_templates::delete_template))
.route("/api/v1/workspaces/{workspace_id}/environments/{env}/promote-scenario", post(handlers::scenario_promotions::promote_scenario))
.route("/api/v1/workspaces/{workspace_id}/promotions", get(handlers::scenario_promotions::list_promotions))
.route("/api/v1/workspaces/{workspace_id}/promotions/{promotion_id}/approve", post(handlers::scenario_promotions::approve_promotion))
.route("/api/v1/workspaces/{workspace_id}/promotions/{promotion_id}/reject", post(handlers::scenario_promotions::reject_promotion))
.route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware))
.route_layer(middleware::from_fn(rate_limit_middleware));
let sso_public_routes = Router::new()
.route("/api/v1/sso/saml/login/{org_slug}", get(handlers::sso::initiate_saml_login))
.route("/api/v1/sso/saml/acs/{org_slug}", post(handlers::sso::saml_acs))
.route("/api/v1/sso/saml/slo/{org_slug}", post(handlers::sso::saml_slo))
.route_layer(middleware::from_fn(rate_limit_middleware));
let oauth_public_routes = Router::new()
.route("/api/v1/auth/oauth/{provider}", get(handlers::oauth::oauth_authorize))
.route("/api/v1/auth/oauth/{provider}/callback", get(handlers::oauth::oauth_callback))
.route_layer(middleware::from_fn(rate_limit_middleware));
let billing_webhook_routes =
Router::new().route("/api/v1/billing/webhook", post(handlers::billing::stripe_webhook));
let admin_routes = Router::new()
.route("/api/v1/admin/plugins/{name}/verify", post(handlers::admin::verify_plugin))
.route("/api/v1/admin/stats", get(handlers::admin::get_admin_stats))
.route("/api/v1/admin/analytics", get(handlers::analytics::get_analytics))
.route(
"/api/v1/admin/analytics/funnel",
get(handlers::analytics::get_conversion_funnel),
)
.route_layer(middleware::from_fn_with_state(state.clone(), auth_middleware))
.route_layer(middleware::from_fn(rate_limit_middleware));
Router::new()
.merge(public_routes)
.merge(sso_public_routes)
.merge(oauth_public_routes)
.merge(billing_webhook_routes)
.merge(auth_routes)
.merge(admin_routes)
}
#[cfg(test)]
mod tests {
#[test]
fn test_public_route_paths() {
let routes = vec![
"/health",
"/health/live",
"/health/ready",
"/api/v1/plugins/search",
"/api/v1/plugins/{name}",
"/api/v1/plugins/{name}/versions/{version}",
"/api/v1/stats",
"/api/v1/auth/register",
"/api/v1/auth/login",
"/api/v1/auth/token/refresh",
"/api/v1/auth/password/reset-request",
"/api/v1/auth/password/reset",
];
for route in routes {
assert!(route.starts_with("/"));
assert!(!route.contains("//"));
}
}
#[test]
fn test_auth_route_paths() {
let routes = vec![
"/api/v1/plugins/publish",
"/api/v1/plugins/{name}/versions/{version}/yank",
"/api/v1/auth/2fa/setup",
"/api/v1/auth/2fa/verify-setup",
"/api/v1/auth/2fa/disable",
"/api/v1/auth/2fa/status",
"/api/v1/organizations",
"/api/v1/organizations/{org_id}",
"/api/v1/organizations/{org_id}/members",
];
for route in routes {
assert!(route.starts_with("/api/v1/"));
assert!(!route.contains("//"));
}
}
#[test]
fn test_sso_route_paths() {
let routes = vec![
"/api/v1/sso/config",
"/api/v1/sso/enable",
"/api/v1/sso/disable",
"/api/v1/sso/saml/metadata/{org_slug}",
"/api/v1/sso/saml/login/{org_slug}",
"/api/v1/sso/saml/acs/{org_slug}",
"/api/v1/sso/saml/slo/{org_slug}",
];
for route in routes {
assert!(route.starts_with("/api/v1/sso"));
assert!(!route.contains("//"));
}
}
#[test]
fn test_admin_route_paths() {
let routes = vec![
"/api/v1/admin/plugins/{name}/verify",
"/api/v1/admin/stats",
"/api/v1/admin/analytics",
"/api/v1/admin/analytics/funnel",
];
for route in routes {
assert!(route.starts_with("/api/v1/admin"));
assert!(!route.contains("//"));
}
}
#[test]
fn test_organization_settings_routes() {
let routes = vec![
"/api/v1/organizations/{org_id}/settings",
"/api/v1/organizations/{org_id}/settings/ai",
"/api/v1/organizations/{org_id}/usage",
"/api/v1/organizations/{org_id}/billing",
];
for route in routes {
assert!(route.contains("{org_id}"));
assert!(route.starts_with("/api/v1/organizations"));
}
}
#[test]
fn test_pillar_analytics_routes() {
let routes = vec![
"/api/v1/organizations/{org_id}/analytics/pillars",
"/api/v1/workspaces/{workspace_id}/analytics/pillars",
"/api/v1/analytics/pillars/events",
];
for route in routes {
assert!(route.contains("analytics"));
assert!(route.contains("pillars"));
}
}
#[test]
fn test_route_parameter_consistency() {
let org_routes = vec![
"/api/v1/organizations/{org_id}",
"/api/v1/organizations/{org_id}/members",
"/api/v1/organizations/{org_id}/settings",
];
for route in org_routes {
assert!(route.contains("{org_id}"));
}
let plugin_routes = vec![
"/api/v1/plugins/{name}",
"/api/v1/plugins/{name}/versions/{version}",
"/api/v1/plugins/{name}/reviews",
];
for route in plugin_routes {
assert!(route.contains("{name}"));
}
}
#[test]
fn test_review_routes() {
let routes = vec![
"/api/v1/plugins/{name}/reviews",
"/api/v1/plugins/{name}/reviews/{review_id}/vote",
];
for route in routes {
assert!(route.contains("reviews"));
assert!(route.contains("{name}"));
}
}
#[test]
fn test_two_factor_routes() {
let routes = vec![
"/api/v1/auth/2fa/setup",
"/api/v1/auth/2fa/verify-setup",
"/api/v1/auth/2fa/disable",
"/api/v1/auth/2fa/status",
];
for route in routes {
assert!(route.contains("/2fa/"));
assert!(route.starts_with("/api/v1/auth"));
}
}
#[test]
fn test_no_duplicate_route_patterns() {
let all_routes = vec![
"/api/v1/plugins/{name}",
"/api/v1/plugins/search",
"/api/v1/organizations/{org_id}",
"/api/v1/organizations/{org_id}/members",
];
let mut seen = std::collections::HashSet::new();
for route in all_routes {
assert!(!seen.contains(route), "Duplicate route: {}", route);
seen.insert(route);
}
}
#[test]
fn test_api_version_consistency() {
let routes = vec![
"/api/v1/plugins/search",
"/api/v1/auth/login",
"/api/v1/organizations",
"/api/v1/admin/stats",
"/api/v1/sso/config",
];
for route in routes {
assert!(route.contains("/api/v1/"));
}
}
#[test]
fn test_route_http_methods_appropriateness() {
struct RouteMethod {
path: &'static str,
should_contain: &'static str,
}
let route_checks = vec![
RouteMethod {
path: "health",
should_contain: "get",
},
RouteMethod {
path: "search",
should_contain: "post",
},
RouteMethod {
path: "publish",
should_contain: "post",
},
RouteMethod {
path: "login",
should_contain: "post",
},
RouteMethod {
path: "register",
should_contain: "post",
},
];
for check in route_checks {
assert!(!check.path.is_empty());
assert!(!check.should_contain.is_empty());
}
}
#[test]
fn test_organization_member_routes() {
let routes = vec![
"/api/v1/organizations/{org_id}/members",
"/api/v1/organizations/{org_id}/members/{user_id}",
];
for route in routes {
assert!(route.contains("members"));
assert!(route.contains("{org_id}"));
}
}
#[test]
fn test_password_reset_routes() {
let routes = vec![
"/api/v1/auth/password/reset-request",
"/api/v1/auth/password/reset",
];
for route in routes {
assert!(route.contains("password"));
assert!(route.contains("reset"));
}
}
#[test]
fn test_saml_routes_org_slug_parameter() {
let routes = vec![
"/api/v1/sso/saml/metadata/{org_slug}",
"/api/v1/sso/saml/login/{org_slug}",
"/api/v1/sso/saml/acs/{org_slug}",
"/api/v1/sso/saml/slo/{org_slug}",
];
for route in routes {
assert!(route.contains("{org_slug}"));
assert!(route.contains("/saml/"));
}
}
#[test]
fn test_plugin_badges_route() {
let route = "/api/v1/plugins/{name}/badges";
assert!(route.contains("{name}"));
assert!(route.contains("badges"));
}
}