Files
iam-service/src/docs.rs
2026-01-31 13:37:15 +08:00

210 lines
7.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String>,
default_token: Option<String>,
}
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 <access_token>".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");
}
}