use crate::handlers; use crate::models::{ CreateRoleRequest, CreateTenantRequest, CreateUserRequest, LoginRequest, LoginResponse, Role, RoleResponse, Tenant, TenantEnabledAppsResponse, TenantResponse, UpdateTenantEnabledAppsRequest, UpdateTenantRequest, UpdateTenantStatusRequest, UpdateUserRequest, UpdateUserRolesRequest, User, UserResponse, }; use serde_json::Value; use utoipa::openapi::security::{HttpAuthScheme, HttpBuilder, SecurityScheme}; use utoipa::{Modify, OpenApi}; struct SecurityAddon; #[derive(Clone)] struct DocsEnvConfig { default_tenant_id: Option, default_token: Option, } impl DocsEnvConfig { fn from_env() -> Self { let require_tenant = std::env::var("IAM_DOCS_REQUIRE_TENANT_ID") .map(|v| v == "1") .unwrap_or(false); let require_token = std::env::var("IAM_DOCS_REQUIRE_TOKEN") .map(|v| v == "1") .unwrap_or(false); let default_tenant_id = std::env::var("IAM_DOCS_DEFAULT_TENANT_ID").ok(); let default_token = std::env::var("IAM_DOCS_DEFAULT_TOKEN").ok(); if require_tenant && default_tenant_id.is_none() { panic!("IAM_DOCS_REQUIRE_TENANT_ID=1 but IAM_DOCS_DEFAULT_TENANT_ID is not set"); } if require_token && default_token.is_none() { panic!("IAM_DOCS_REQUIRE_TOKEN=1 but IAM_DOCS_DEFAULT_TOKEN is not set"); } Self { default_tenant_id, default_token, } } fn tenant_example(&self) -> String { self.default_tenant_id .clone() .unwrap_or_else(|| "11111111-1111-1111-1111-111111111111".to_string()) } fn authorization_example(&self) -> String { let Some(token) = self.default_token.clone() else { return "Bearer ".to_string(); }; if token.to_ascii_lowercase().starts_with("bearer ") { token } else { format!("Bearer {}", token) } } } impl Modify for SecurityAddon { fn modify(&self, openapi: &mut utoipa::openapi::OpenApi) { let components = openapi .components .get_or_insert_with(utoipa::openapi::Components::new); components.add_security_scheme( "bearer_auth", SecurityScheme::Http( HttpBuilder::new() .scheme(HttpAuthScheme::Bearer) .bearer_format("JWT") .build(), ), ); let cfg = DocsEnvConfig::from_env(); apply_header_parameter_examples(openapi, &cfg); } } fn apply_header_parameter_examples(openapi: &mut utoipa::openapi::OpenApi, cfg: &DocsEnvConfig) { use utoipa::openapi::path::ParameterBuilder; use utoipa::openapi::path::ParameterIn; let tenant_value = cfg.tenant_example(); let auth_value = cfg.authorization_example(); for (_path, item) in openapi.paths.paths.iter_mut() { let operations = [ item.get.as_mut(), item.post.as_mut(), item.put.as_mut(), item.patch.as_mut(), item.delete.as_mut(), ]; for op in operations.into_iter().flatten() { let Some(params) = op.parameters.as_mut() else { continue; }; for param in params.iter_mut() { if param.parameter_in == ParameterIn::Header && param.name.eq_ignore_ascii_case("X-Tenant-ID") { let builder: ParameterBuilder = std::mem::take(param).into(); *param = builder .example(Some(Value::String(tenant_value.clone()))) .build(); } if param.parameter_in == ParameterIn::Header && param.name.eq_ignore_ascii_case("Authorization") { let builder: ParameterBuilder = std::mem::take(param).into(); *param = builder .example(Some(Value::String(auth_value.clone()))) .build(); } } } } } #[derive(OpenApi)] #[openapi( modifiers(&SecurityAddon), info( title = "IAM Service API", version = "0.1.0", description = include_str!("../docs/SCALAR_GUIDE.md") ), paths( handlers::auth::register_handler, handlers::auth::login_handler, handlers::authorization::my_permissions_handler, handlers::platform::get_tenant_enabled_apps_handler, handlers::platform::set_tenant_enabled_apps_handler, handlers::tenant::create_tenant_handler, handlers::tenant::get_tenant_handler, handlers::tenant::update_tenant_handler, handlers::tenant::update_tenant_status_handler, handlers::tenant::delete_tenant_handler, handlers::role::create_role_handler, handlers::role::list_roles_handler, handlers::user::list_users_handler, handlers::user::get_user_handler, handlers::user::update_user_handler, handlers::user::delete_user_handler, handlers::user::list_user_roles_handler, handlers::user::set_user_roles_handler, // Add other handlers here as you implement them ), components( schemas( User, UserResponse, CreateUserRequest, UpdateUserRequest, LoginRequest, LoginResponse, Role, CreateRoleRequest, RoleResponse, Tenant, TenantResponse, CreateTenantRequest, UpdateTenantRequest, UpdateTenantStatusRequest, UpdateTenantEnabledAppsRequest, TenantEnabledAppsResponse, UpdateUserRolesRequest ) ), tags( (name = "Auth", description = "认证:注册/登录/令牌"), (name = "Tenant", description = "租户:创建/查询/更新/状态/删除"), (name = "User", description = "用户:查询/列表/更新/删除(需权限)"), (name = "Role", description = "角色:创建/列表(需权限)"), (name = "Me", description = "当前用户:权限自查等"), (name = "Policy", description = "策略:预留(ABAC/策略引擎后续扩展)") ) )] pub struct ApiDoc; #[cfg(test)] mod tests { use super::ApiDoc; use utoipa::OpenApi; #[test] fn openapi_schema_contains_defaults() { let doc = ApiDoc::openapi(); let json = serde_json::to_value(&doc).unwrap(); let token_type_default = json .pointer("/components/schemas/LoginResponse/properties/token_type/default") .and_then(|v| v.as_str()) .unwrap_or_default(); assert_eq!(token_type_default, "Bearer"); let tenant_status_default = json .pointer("/components/schemas/Tenant/properties/status/default") .and_then(|v| v.as_str()) .unwrap_or_default(); assert_eq!(tenant_status_default, "active"); } }