fix(doc): fix doc

This commit is contained in:
2026-01-31 13:37:15 +08:00
parent d071e1a27d
commit 6b68a368f1
10 changed files with 158 additions and 17 deletions

View File

@@ -5,11 +5,61 @@ use crate::models::{
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
@@ -24,6 +74,51 @@ impl Modify for SecurityAddon {
.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();
}
}
}
}
}
@@ -35,16 +130,7 @@ impl Modify for SecurityAddon {
version = "0.1.0",
description = include_str!("../docs/SCALAR_GUIDE.md")
),
servers(
(
url = "https://{env}/api",
description = "Environment server",
variables(
("env" = (default = "dev", enum_values("dev", "staging", "prod"))),
("port" = (default = "5010"))
)
)
),
paths(
handlers::auth::register_handler,
handlers::auth::login_handler,

View File

@@ -5,7 +5,8 @@ use axum::{Json, extract::State};
use common_telemetry::{AppError, AppResponse};
use tracing::instrument;
/// 注册接口
/// Register (create user in tenant).
/// 注册接口(在租户下创建用户)。
#[utoipa::path(
post,
path = "/auth/register",
@@ -40,7 +41,8 @@ pub async fn register_handler(
Ok(AppResponse::created(response))
}
/// 登录接口
/// Login (issue access token).
/// 登录接口(签发访问令牌)。
#[utoipa::path(
post,
path = "/auth/login",

View File

@@ -23,6 +23,7 @@ use tracing::instrument;
)
)]
#[instrument(skip(state))]
/// List current user's permissions in current tenant.
/// 查询当前登录用户在当前租户下的权限编码列表。
///
/// 用途:

View File

@@ -9,6 +9,8 @@ use common_telemetry::{AppError, AppResponse};
use tracing::instrument;
use uuid::Uuid;
/// Get tenant enabled apps (platform scope).
/// 平台层查询租户已开通应用enabled_apps
#[utoipa::path(
get,
path = "/platform/tenants/{tenant_id}/enabled-apps",
@@ -46,6 +48,8 @@ pub async fn get_tenant_enabled_apps_handler(
}))
}
/// Set tenant enabled apps (platform scope, full replace).
/// 平台层设置租户已开通应用enabled_apps全量覆盖
#[utoipa::path(
put,
path = "/platform/tenants/{tenant_id}/enabled-apps",
@@ -89,4 +93,3 @@ pub async fn set_tenant_enabled_apps_handler(
updated_at,
}))
}

View File

@@ -26,6 +26,7 @@ use tracing::instrument;
)
)]
#[instrument(skip(state, payload))]
/// Create role in current tenant.
/// 在当前租户下创建角色。
///
/// 业务规则:
@@ -87,6 +88,7 @@ pub async fn create_role_handler(
)
)]
#[instrument(skip(state))]
/// List roles in current tenant.
/// 查询当前租户下的角色列表。
///
/// 业务规则:

View File

@@ -19,6 +19,7 @@ use tracing::instrument;
)
)]
#[instrument(skip(state, payload))]
/// Create tenant (public endpoint).
/// 创建租户(公开接口)。
///
/// 业务规则:
@@ -65,6 +66,7 @@ pub async fn create_tenant_handler(
)
)]
#[instrument(skip(state))]
/// Get current tenant info.
/// 获取当前登录用户所属租户的信息。
///
/// 业务规则:
@@ -127,6 +129,7 @@ pub async fn get_tenant_handler(
)
)]
#[instrument(skip(state, payload))]
/// Update current tenant (name / config).
/// 更新当前租户的基础信息(名称 / 配置)。
///
/// 业务规则:
@@ -194,6 +197,7 @@ pub async fn update_tenant_handler(
)
)]
#[instrument(skip(state, payload))]
/// Update current tenant status (e.g. active / disabled).
/// 更新当前租户状态(如 active / disabled
///
/// 业务规则:
@@ -260,6 +264,7 @@ pub async fn update_tenant_status_handler(
)
)]
#[instrument(skip(state))]
/// Delete current tenant.
/// 删除当前租户。
///
/// 业务规则:

View File

@@ -38,6 +38,7 @@ pub struct ListUsersQuery {
)
)]
#[instrument(skip(state))]
/// List users in tenant with pagination.
/// 分页查询当前租户下的用户列表。
///
/// 业务规则:
@@ -114,6 +115,7 @@ pub async fn list_users_handler(
)
)]
#[instrument(skip(state))]
/// Get user by id.
/// 根据用户 ID 查询用户详情。
///
/// 业务规则:
@@ -181,6 +183,7 @@ pub async fn get_user_handler(
)
)]
#[instrument(skip(state, payload))]
/// Update user (currently supports updating email).
/// 更新指定用户信息(目前支持更新邮箱)。
///
/// 业务规则:
@@ -250,6 +253,7 @@ pub async fn update_user_handler(
)
)]
#[instrument(skip(state))]
/// Delete user.
/// 删除指定用户。
///
/// 业务规则:
@@ -312,6 +316,7 @@ pub async fn delete_user_handler(
)
)]
#[instrument(skip(state))]
/// List roles bound to a user.
/// 查询用户已绑定的角色列表。
///
/// 业务规则:
@@ -380,6 +385,7 @@ pub async fn list_user_roles_handler(
)
)]
#[instrument(skip(state, payload))]
/// Set user's roles (full replace, idempotent).
/// 设置用户的角色绑定(全量覆盖,幂等)。
///
/// 业务规则:

View File

@@ -367,10 +367,11 @@ impl TenantService {
if enabled_apps.is_empty() {
return Ok(());
}
let rows: Vec<String> = sqlx::query_scalar("SELECT id FROM apps WHERE id = ANY($1)")
.bind(enabled_apps)
.fetch_all(&self.pool)
.await?;
let rows: Vec<String> =
sqlx::query_scalar("SELECT id FROM apps WHERE id = ANY($1) AND status = 'active'")
.bind(enabled_apps)
.fetch_all(&self.pool)
.await?;
let found: HashSet<String> = rows.into_iter().collect();
for app in enabled_apps {
if !found.contains(app) {