diff --git a/cmd/server/main.go b/cmd/server/main.go index 774dfa8..b140166 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -3,10 +3,16 @@ package main import ( "log" + _ "github.com/user-management-system/docs" "github.com/user-management-system/internal/config" "github.com/user-management-system/internal/server" ) +// @title User Management System API +// @version 1.0 +// @description API for user management, authentication, authorization, and administration. +// @BasePath /api/v1 +// @schemes http https func main() { // 加载配置 cfg, err := config.Load() diff --git a/docs/archive/OAUTH_INTEGRATION.md b/docs/archive/OAUTH_INTEGRATION.md index f9da158..5789ef0 100644 --- a/docs/archive/OAUTH_INTEGRATION.md +++ b/docs/archive/OAUTH_INTEGRATION.md @@ -33,14 +33,16 @@ cp configs/oauth_config.example.yaml configs/oauth_config.yaml # 示例:微信配置 wechat: enabled: true - app_id: "wx1234567890abcdef" - app_secret: "1234567890abcdef1234567890abcdef" + app_id: "" + app_secret: "" # 示例:Google配置 google: enabled: true - client_id: "123456789-abcdef.apps.googleusercontent.com" - client_secret: "GOCSPX-abcdef123456" + client_id: "" + client_secret: "" + + ``` ### 3. 数据库迁移 @@ -290,13 +292,13 @@ Authorization: Bearer ```bash # 微信 WECHAT_OAUTH_ENABLED=true -WECHAT_APP_ID=wx1234567890abcdef -WECHAT_APP_SECRET=1234567890abcdef1234567890abcdef +WECHAT_APP_ID= +WECHAT_APP_SECRET= # Google GOOGLE_OAUTH_ENABLED=true -GOOGLE_CLIENT_ID=123456789-abcdef.apps.googleusercontent.com -GOOGLE_CLIENT_SECRET=GOCSPX-abcdef123456 +GOOGLE_CLIENT_ID= +GOOGLE_CLIENT_SECRET= # Facebook FACEBOOK_OAUTH_ENABLED=true diff --git a/docs/code-review/FULL_REVIEW_2026-05-30.md b/docs/code-review/FULL_REVIEW_2026-05-30.md new file mode 100644 index 0000000..8711801 --- /dev/null +++ b/docs/code-review/FULL_REVIEW_2026-05-30.md @@ -0,0 +1,561 @@ +# user-system 全面 Review 报告 + +**审查日期**:2026-05-30 +**审查范围**:`/home/long/project/user-system` +**审查模式**:严格、系统、全面 +**审查方式**:源码审阅 + 实际构建/测试/静态检查验证 + 第二轮契约一致性对账 +**结论等级**:**B- / 有条件可运行,不可宣称“已全面收口”** + +--- + +## 一、执行摘要 + +该项目不是不可用项目。后端、前端、测试主链路均可运行,说明系统已经具备较高完成度;但它距离“高可靠、可审计、严格闭环”的标准仍有明显差距,主要集中在以下五类问题: + +1. **SSO/OAuth 协议正确性存在关键缺口** +2. **Swagger / 路由 / 文档之间存在系统性契约漂移** +3. **测试数量很多,但契约强度不足,且掩盖了真实路由/鉴权问题** +4. **质量门禁对外表述与实际状态不一致** +5. **缓存失效、参数校验、上传实现等边界质量仍不够严谨** + +一句话结论: + +> 当前项目可以诚实表述为“主体功能可运行、可测试,但仍存在高价值安全与契约治理缺口”;不能诚实表述为“严格闭环、全面审计通过”。 + +--- + +## 二、审查范围与方法 + +### 2.1 重点审查模块 + +- 启动与配置链路 + - `cmd/server/main.go` + - `internal/server/server.go` + - `internal/config/config.go` +- 认证 / 授权 / 会话 + - `internal/api/middleware/auth.go` + - `internal/service/auth.go` + - `internal/service/user_service.go` + - `internal/auth/sso.go` + - `internal/api/handler/sso_handler.go` +- 核心 Handler 与 API 暴露 + - `internal/api/handler/user_handler.go` + - `internal/api/handler/export_handler.go` + - `internal/api/handler/avatar_handler.go` + - `internal/api/router/router.go` +- 仓储层 + - `internal/repository/user.go` + - `internal/repository/operation_log.go` +- 前端契约与测试 + - `frontend/admin/src/services/*` + - `frontend/admin/src/pages/admin/ImportExportPage/*` + - `internal/api/handler/*_test.go` + - `internal/e2e/*` +- 文档与 Swagger + - `docs/swagger.go` + - `docs/docs.go` + - `docs/API.md` + - `docs/archive/OAUTH_INTEGRATION.md` + +### 2.2 第二轮差异化审查方法 + +除第一轮常规源码审阅外,第二轮增加了以下“不同方式”的 review: + +1. **路由注册 vs Swagger 注释逐项对账** + - 以 `internal/api/router/router.go` 为真实路由基准 + - 对照 `internal/api/handler/*.go` 中所有 `@Router` 注释 +2. **协议路径 vs 鉴权模型对账** + - 重点检查 SSO `/authorize`、`/token`、`/introspect`、`/revoke`、`/userinfo` + - 核对它们是否被挂在了正确的 middleware / route group 下 +3. **测试行为 vs 真实路由语义对账** + - 检查测试是否在错误的前提下仍“允许通过” +4. **文档路径 vs 前端调用路径对账** + - 对照 Swagger 注释、路由、前端 service、API 文档的四方一致性 + +第二轮发现了**新的系统性问题**,已补充到本报告和修复计划中。 + +--- + +## 三、实际执行的验证 + +以下命令已实际执行。 + +### 3.1 通过项 + +```bash +go test ./... -count=1 +go build ./cmd/server +cd frontend/admin && env -u NODE_ENV npm run test:run +cd frontend/admin && env -u NODE_ENV npm run build +``` + +结果: + +- `go test ./... -count=1`:**通过** +- `go build ./cmd/server`:**通过** +- 前端 `npm run test:run`:**通过** + - `82 files / 525 tests` +- 前端 `npm run build`:**通过** + +### 3.2 失败项 + +```bash +go vet ./... +``` + +结果:**失败** + +失败位置: + +- `internal/api/handler/avatar_handler_test.go:204` +- `internal/api/handler/export_handler_test.go:174` +- `internal/api/handler/export_handler_test.go:202` +- `internal/api/handler/export_handler_test.go:229` + +失败信息: + +- `using resp before checking for errors` + +这说明当前仓库不能继续对外宣称 `go vet` 已通过。 + +--- + +## 四、主要发现 + +--- + +## P0:必须优先修复的问题 + +### P0-1:Swagger 文档实际为空壳,当前不能算有效 API 文档 + +**证据**: + +`docs/swagger.go` 中: + +```json +"paths": {} +``` + +同时 `internal/api/router/router.go` 公开暴露了: + +- `/swagger/*any` + +**影响**: + +- Swagger UI 可能可访问 +- 但 API spec 本身没有有效路径 +- “Swagger 已完成”是错误表述 + +**结论**:高优先级治理缺陷。 + +--- + +### P0-2:Swagger 注释与真实路由存在系统性漂移,不是单点问题 + +第一轮只确认了导入导出接口漂移;第二轮确认:**这不是局部问题,而是全局契约漂移**。 + +**明确证据示例**: + +1. **导入导出接口** + - 注释:`/api/v1/exports/users`、`/api/v1/exports/template` + - 实际:`/api/v1/admin/users/export`、`/api/v1/admin/users/import`、`/api/v1/admin/users/import/template` + +2. **刷新令牌接口** + - 注释:`/api/v1/auth/refresh-token` + - 实际:`/api/v1/auth/refresh` + +3. **邮箱验证码登录接口** + - 注释:`/api/v1/auth/login-by-email-code` + - 实际:`/api/v1/auth/login/email-code` + +4. **重发激活邮件接口** + - 注释:`/api/v1/auth/resend-activation-email` + - 实际:`/api/v1/auth/resend-activation` + +5. **TOTP / 2FA 接口** + - 注释:`/api/v1/auth/totp/*` + - 实际:`/api/v1/auth/2fa/*` + - 且 `SetupTOTP` 注释是 `POST`,实际路由是 `GET` + +6. **Captcha 接口** + - 注释:`/api/v1/captcha/*` + - 实际:`/api/v1/auth/captcha*` + +7. **密码重置接口** + - 注释:`/api/v1/auth/password/forgot`、`/reset` 等 + - 实际:`/api/v1/auth/forgot-password`、`/reset-password`、`/forgot-password/phone` + +8. **自定义字段接口** + - 注释:`/api/v1/fields/*` + - 实际:`/api/v1/custom-fields/*` + +9. **日志接口** + - 注释:`/api/v1/users/me/login-logs`、`/operation-logs` + - 实际:`/api/v1/logs/login/me`、`/api/v1/logs/operation/me` + +10. **管理员接口** + - 注释:`/api/v1/users/admins` + - 实际:`/api/v1/admin/admins` + +11. **方法不一致** + - `AssignRoles` 注释为 `POST /api/v1/users/{id}/roles`,实际是 `PUT` + - `AssignPermissions` 注释为 `POST /api/v1/roles/{id}/permissions`,实际是 `PUT` + +**影响**: + +- 当前 Swagger 注释整体**不可信** +- 不能基于其生成正确 SDK 或自动化客户端 +- 文档、前端、后端、测试之间存在多套契约 +- 即使把 Swagger 重新生成,也仍会生成错误契约,除非先修注释 + +**结论**:严重契约一致性问题。 + +--- + +### P0-3:SSO 授权码没有绑定 redirect_uri,token 兑换阶段未校验 redirect_uri / code / client 三元绑定 + +**证据**: + +`internal/auth/sso.go` 中 `SSOSession` 结构体不包含 `RedirectURI` 字段。 + +`GenerateAuthorizationCode(clientID, redirectURI, scope, ...)` 虽接收 `redirectURI`,但没有保存到 session。 + +`internal/api/handler/sso_handler.go` 的 `Token` 流程中: + +- 校验了 `grant_type` +- 校验了 `client_secret` +- 校验了 `code` 是否存在 +- **未校验** `req.RedirectURI == session.RedirectURI` +- **未做严格的 code-client-redirect 三元绑定** + +**影响**: + +- 授权码模式协议实现不完整 +- 授权码被截获或混用时,服务端缺少关键约束 +- 不满足高可靠安全要求 + +**结论**:严重安全问题。 + +--- + +### P0-4:SSO implicit flow 仍被支持,并通过 URL fragment 返回 access token + +**证据**: + +`internal/api/handler/sso_handler.go` 中,当 `response_type == "token"` 时: + +```go +redirectURL := req.RedirectURI + "#access_token=" + token + "&expires_in=7200" +``` + +**影响**: + +- access token 暴露给前端地址片段 +- 不适合高安全系统 +- 与现代 OAuth 推荐实践不一致 + +**结论**:严重安全设计问题。 + +--- + +### P0-5:SSO `/token`、`/introspect`、`/revoke`、`/userinfo` 被挂在错误的鉴权模型下,协议语义与访问控制同时出错 + +这是第二轮新增的关键发现。 + +**证据**: + +`internal/api/router/router.go` 中: + +- SSO 整组被挂在: + - `protected := v1.Group("")` + - `protected.Use(r.authMiddleware.Required())` +- 然后: + - `sso := protected.Group("/sso")` + - `sso.POST("/token", r.ssoHandler.Token)` + - `sso.POST("/introspect", r.ssoHandler.Introspect)` + - `sso.POST("/revoke", r.ssoHandler.Revoke)` + - `sso.GET("/userinfo", r.ssoHandler.UserInfo)` + +而对应 handler 语义是: + +- `Token`:使用 `grant_type + code + client_id + client_secret` 兑换 token,不依赖当前登录用户 +- `Introspect`:只收 `token` / `client_id` +- `Revoke`:只收 `token` +- `UserInfo`:当前实现反而直接读 app auth middleware 注入的 `user_id` / `username` + +**影响**: + +1. **OAuth 客户端无法按协议直接兑换授权码** + - 因为 `/token` 被错误地要求先通过平台 BearerAuth +2. **`/introspect` 与 `/revoke` 不是 client-auth 模型,而是 app-user-auth 模型** + - 任意已登录平台用户如果拿到 token 字符串,就可能执行 introspect / revoke +3. **`/userinfo` 返回的是平台 JWT 上下文中的用户,而不是 SSO access token 的 subject** + - 协议语义错误 +4. **现有测试已经在掩盖这个问题** + - 测试里直接不带认证访问 `/api/v1/sso/token`、`/introspect`、`/revoke` + - 但断言允许 200/400/401 多种状态混过 + +**结论**:严重的协议与访问控制双重错误,必须优先修复。 + +--- + +## P1:应尽快修复的问题 + +### P1-1:测试大量使用“宽松状态码断言”,无法守住真实接口契约 + +**证据**: + +`internal/api/handler/export_handler_test.go`、`internal/api/handler/sso_handler_test.go` 中大量断言允许: + +- 200 +- 302 +- 400 +- 401 +- 403 +- 500 + +中的多个同时通过。 + +**第二轮补充证据**: + +- `sso_handler_test.go` 中多处直接对 `/api/v1/sso/token`、`/introspect`、`/revoke` 发起**无认证请求** +- 但测试依旧允许 `401`、`400`、`200` 等多个互斥结果 +- 这恰好掩盖了 `router.go` 中 SSO route group 被错误挂到 `protected` 下的问题 + +**影响**: + +- 测试数量多但行为约束弱 +- 路由语义漂移、鉴权模型错误时测试仍可能全绿 +- 会制造“测试全绿”的假象 + +**结论**:高优先级测试质量问题。 + +--- + +### P1-2:`go vet ./...` 实际不通过,项目对外表述与真实状态不一致 + +**证据**: + +本次实际执行 `go vet ./...` 失败,失败点见第三节。 + +**影响**: + +- README 与状态文档中若继续宣称 `go vet PASS`,属于事实不符 +- 静态分析未真正成为质量门禁 + +**结论**:高优先级工程质量问题。 + +--- + +### P1-3:JWT secret 治理与项目自我标准不完全一致 + +**证据**: + +`cmd/server/main.go` 使用 `config.Load()`,不是 `LoadForBootstrap()`,这点是好的;但 `internal/config/config.go` 中对弱 JWT secret 仅见 `warn` 级处理证据,而未见 release 模式弱值硬失败证据。 + +仓库多份 review / 标准文档则明确要求: + +- 生产环境通过环境变量注入 `JWT_SECRET` +- 缺失 / 弱值应 fatal + +**影响**: + +- 代码行为与治理标准之间存在差距 +- 高可靠环境下,弱密钥仅告警不足够 + +**结论**:重要安全治理问题。 + +--- + +### P1-4:用户状态 / 权限缓存失效接口存在,但未见业务路径接入证据 + +**证据**: + +`internal/api/middleware/auth.go` 暴露了: + +- `InvalidateUserStateCache(userID)` +- `InvalidateUserPermCache(userID)` + +但在 service / handler / server 调用链中未找到这些失效方法的业务接入证据。 + +同时缓存 TTL 为: + +- 用户状态:5s +- 权限缓存:5min + +**影响**: + +- 密码修改、状态修改、角色修改、权限调整后可能短时继续沿用旧授权结果 +- 在高敏感场景中不够严格 + +**结论**:重要一致性问题。 + +--- + +### P1-5:归档文档中存在拟真 OAuth secret 示例,文档边界不干净 + +**证据**: + +`docs/archive/OAUTH_INTEGRATION.md` 中存在: + +```yaml +client_secret: "GOCSPX-abcdef123456" +``` + +**影响**: + +- 容易被误判为真实 secret +- 不符合敏感信息示例占位规范 + +**结论**:文档安全卫生问题。 + +--- + +## P2:建议优化的问题 + +### P2-1:`strconvAtoi` 非法输入返回 `(0, nil)`,会吞掉参数错误 + +**证据**: + +`internal/api/handler/export_handler.go` 中: + +```go +if c < '0' || c > '9' { + return 0, nil +} +``` + +这会把非法 `status=abc` 静默转换成 `0`。 + +**影响**: + +- 参数错误被吞掉 +- 查询语义可能被扭曲 + +**结论**:中优先级正确性问题。 + +--- + +### P2-2:头像上传一次性读入整个文件,不必要 + +**证据**: + +`internal/api/handler/avatar_handler.go`: + +```go +data := make([]byte, file.Size) +src.Read(data) +os.WriteFile(dstPath, data, 0o644) +``` + +**影响**: + +- 不必要的整块内存分配 +- 虽当前 5MB 限制可控,但实现不够稳健 + +**结论**:中优先级实现质量问题。 + +--- + +### P2-3:头像上传成功响应使用匿名 `gin.H`,接口 schema 易漂移 + +**证据**: + +`internal/api/handler/avatar_handler.go` 返回: + +```go +"data": gin.H{ + "avatar_url": avatarURL, + "thumbnail": avatarURL, +} +``` + +但注释中宣称的是 `AvatarResponse`。 + +**影响**: + +- 文档与实现松耦合 +- 前端类型契约不稳 + +**结论**:中优先级可维护性问题。 + +--- + +## 五、值得保留的正面设计 + +### 5.1 头像上传做了扩展名 + Magic Bytes 双校验 +位置:`internal/api/handler/avatar_handler.go` + +这是正确的防伪装上传设计。 + +### 5.2 LIKE 搜索做了特殊字符转义 +位置: +- `internal/repository/user.go` +- `internal/repository/operation_log.go` + +说明对模式匹配误用和干扰有明确防御意识。 + +### 5.3 权限查询做了合并查询 + 缓存 +位置:`internal/api/middleware/auth.go` + +方向正确,说明系统已考虑权限查询成本。 + +### 5.4 密码修改事务中避免重复 Argon2id 计算 +位置:`internal/service/user_service.go` + +这体现了不错的成本意识与事务处理意识。 + +### 5.5 前端对原生弹窗做了 guard +位置:`frontend/admin/src/app/bootstrap/installWindowGuards.ts` + +与仓库“禁止原生 alert/confirm/prompt/open”的规则一致。 + +--- + +## 六、测试体系评估 + +### 6.1 测试“很多”,但不等于“严格” + +当前问题不是缺测试,而是: + +- 测试覆盖面不算窄 +- 但很多 handler 测试不对行为做强约束 +- 真实接口契约未被有效锁定 + +### 6.2 E2E 有价值,但仍偏“可访问性验证” + +`internal/e2e/e2e_advanced_test.go` 已对真实 admin 导出路由做访问限制验证,这是正面项;但协议严谨性、返回结构一致性、错误语义边界仍缺少强验证。 + +### 6.3 第二轮确认:测试还在掩盖路由/鉴权模型错误 + +SSO 相关测试已经直接暴露出一个事实: + +- 被测接口在路由层要求平台 BearerAuth +- 测试却在无认证前提下继续跑 +- 断言又接受 200/400/401 多种结果 + +这类测试不是“有弹性”,而是**无法担任回归保护**。 + +### 6.4 `go vet` 尚未纳入真实闭环 + +当前最直接证据就是:`go vet ./...` 失败,而项目文档却可能继续声称通过。 + +--- + +## 七、最终结论 + +该项目: + +- **可以运行** +- **可以构建** +- **大部分测试可以通过** +- **但仍不能宣称“严格闭环、全面收口、可全面审计通过”** + +最关键的阻塞点不是“功能没做完”,而是: + +1. **SSO/OAuth 协议与路由鉴权模型不够严谨** +2. **Swagger / 路由 / 文档契约漂移是系统性的,不是局部的** +3. **测试绿但不够硬,且会掩盖真实问题** +4. **静态检查门禁未真正闭环** + +建议下一步按修复计划先处理 P0,再收紧测试与门禁,最后同步更新状态文档与对外表述。 \ No newline at end of file diff --git a/docs/code-review/REMEDIATION_PLAN_2026-05-30.md b/docs/code-review/REMEDIATION_PLAN_2026-05-30.md new file mode 100644 index 0000000..0b2aca0 --- /dev/null +++ b/docs/code-review/REMEDIATION_PLAN_2026-05-30.md @@ -0,0 +1,436 @@ +# user-system 修复执行计划(按 P0 / P1 / P2 排序) + +**计划日期**:2026-05-30 +**输入依据**:`docs/code-review/FULL_REVIEW_2026-05-30.md` +**目标**:修复本轮 review 暴露出的安全、正确性、测试与文档一致性问题,并形成新的可审计验证证据。 + +--- + +## 一、执行原则 + +1. **先修协议与契约,再修测试与文档** + - 先修 SSO / Swagger / 路由契约错误 + - 再收敛测试与静态检查 +2. **每一类问题修完都必须立即验证** +3. **文档只能反映已验证事实,不能提前宣称完成** +4. **对外可见契约必须单点真实** + - 路由 + - Swagger + - 前端调用 + - 测试断言 + - 状态文档 +5. **修复计划必须覆盖 review 报告中的全部问题** + - 不能只修“代表性问题” + - 必须处理系统性问题源头 + +--- + +## 二、P0 修复计划(必须最优先) + +### P0-1:把空壳 Swagger 修成真实有效文档 + +#### 目标 +让 `/swagger/*any` 对应的不是空 `paths`,而是真实可用 OpenAPI 文档。 + +#### 具体动作 +1. 梳理 Swagger 生成入口与当前生成流程 +2. 确认 `swag init` 或项目既定生成方式 +3. 生成有效 `docs/swagger.go` / `docs/docs.go` +4. 校验 `paths` 非空 +5. 校验至少以下路径存在: + - `/api/v1/auth/login` + - `/api/v1/auth/register` + - `/api/v1/admin/users/export` + - `/api/v1/users/{id}` + +#### 验证 +- 生成 Swagger +- 检查 `docs/swagger.go` 中 `paths` 非空 +- 如可本地启动,验证 `/swagger/index.html` 与 `/swagger/doc.json` 可用 + +--- + +### P0-2:系统性修正 Swagger 注释与真实路由的漂移 + +> 这是对报告中“系统性契约漂移”的完整修复,不再只处理导入导出接口。 + +#### 目标 +统一以下来源的 API 契约: + +- `internal/api/router/router.go` +- `internal/api/handler/*.go` 中全部 `@Router` +- `docs/API.md` +- 前端调用与测试 +- 生成后的 Swagger 文档 + +#### 具体动作 +1. 全量审计并修复以下类别的 `@Router` 漂移: + - export/import:admin 路径 + - refresh:`/refresh-token` → `/refresh` + - email-code login:`/login-by-email-code` → `/login/email-code` + - resend activation:`/resend-activation-email` → `/resend-activation` + - TOTP:`/auth/totp/*` → `/auth/2fa/*` + - captcha:`/captcha/*` → `/auth/captcha*` + - password reset:`/auth/password/*` → `/forgot-password` / `/reset-password` / phone 变体 + - custom fields:`/fields/*` → `/custom-fields/*` + - logs:`/users/me/*logs` → `/logs/*/me` + - admins:`/users/admins` → `/admin/admins` + - users/me 绑定类接口:bind-email / bind-phone / social accounts +2. 修复 HTTP method 漂移: + - `AssignRoles`:`POST` → `PUT` + - `AssignPermissions`:`POST` → `PUT` + - `SetupTOTP`:注释 method 与真实 method 对齐 +3. 对照 `router.go` 做一次全量注释-路由对账,直到关键差异清零 +4. 更新 `docs/API.md` 中对应路径 +5. 重新生成 Swagger 文档 + +#### 验证 +- `go test ./internal/api/handler ./internal/api/router -count=1` +- 生成 Swagger 后检查关键路径与 method 全部正确 +- 使用脚本或审查清单确认:关键业务路由不再存在注释/注册漂移 + +--- + +### P0-3:修复 SSO 授权码模式未绑定 `redirect_uri` 的问题 + +#### 目标 +让 authorization code 与 client / redirect URI 形成强绑定。 + +#### 具体动作 +1. 在 `internal/auth/sso.go` 的 `SSOSession` 中加入 `RedirectURI` +2. `GenerateAuthorizationCode(...)` 保存该字段 +3. `Token(...)` 兑换令牌时校验: + - `session.ClientID == req.ClientID` + - `session.RedirectURI == req.RedirectURI` +4. 对不匹配场景返回明确错误 +5. 为此补回归测试 + +#### 验证 +- `go test ./internal/auth ./internal/api/handler -count=1` +- 增加测试覆盖: + - 正确 client + redirect_uri 成功 + - 错误 redirect_uri 失败 + - 错误 client_id 失败 + +--- + +### P0-4:禁用 implicit flow + +#### 目标 +系统只支持更安全的授权码模式,不再通过 fragment 返回 access token。 + +#### 具体动作 +1. 修改 `internal/api/handler/sso_handler.go` +2. 对 `response_type=token`: + - 返回 `400 unsupported response_type` + - 或仅允许 `code` +3. 清理相应的宽松测试 +4. 同步文档说明只支持 code flow + +#### 验证 +- `response_type=token` 应明确失败 +- `response_type=code` 正常工作 + +--- + +### P0-5:重构 SSO 路由分组与鉴权模型,使 `/token`、`/introspect`、`/revoke`、`/userinfo` 语义正确 + +> 这是第二轮新增问题;若不修,P0-3/P0-4 仍不完整。 + +#### 目标 +让 SSO/OAuth 相关端点符合正确的访问控制模型,而不是错误复用平台用户 BearerAuth。 + +#### 具体动作 +1. 将 SSO 路由按语义拆分,不再整体挂在 `protected` 下 +2. 至少区分: + - `/authorize`:需要当前平台登录用户完成授权 + - `/token`:客户端凭证 + 授权码模型,不依赖当前平台 BearerAuth + - `/introspect`:客户端认证模型 + - `/revoke`:客户端认证模型或 token-owner 受控模型,必须明确 + - `/userinfo`:基于 SSO access token,而不是平台 JWT 上下文 +3. 为 `/token`、`/introspect`、`/revoke` 设计明确的 client auth 机制 +4. 修正 `UserInfo` 的 token 解析来源,不能继续直接读平台 auth middleware 的 `user_id` +5. 同步更新测试与文档 + +#### 验证 +- `/token` 在无平台 BearerAuth、仅有正确 client/code 条件下可成功 +- `/introspect` / `/revoke` 不接受任意平台登录用户代操作 +- `/userinfo` 返回的是 SSO token subject,而不是平台当前 session user + +--- + +## 三、P1 修复计划(紧随 P0) + +### P1-1:修复 `go vet ./...` 失败并收口静态分析门禁 + +#### 目标 +让项目重新具备诚实宣称 `go vet` 通过的资格。 + +#### 具体动作 +1. 修复: + - `internal/api/handler/avatar_handler_test.go` + - `internal/api/handler/export_handler_test.go` +2. 所有 `resp` 使用前先检查 `err` +3. 扫描同类 helper/测试模式,避免只修报错行 + +#### 验证 +- `go vet ./...` +- `go test ./... -count=1` + +--- + +### P1-2:把宽松状态码测试改成严格契约测试 + +#### 目标 +让测试真正约束行为,而不是“什么都算通过”。 + +#### 具体动作 +1. 优先重写以下测试文件: + - `internal/api/handler/export_handler_test.go` + - `internal/api/handler/sso_handler_test.go` +2. 逐场景收紧断言: + - 未认证 → 401 + - 未授权 → 403 + - 参数错误 → 400 + - 成功 → 200 / 302 +3. 删除允许 `500` 的正常断言路径 +4. 对有环境差异的场景,先修被测逻辑,再收紧测试 +5. 针对 SSO 补充协议级回归测试: + - `/token` 不再被平台 BearerAuth 门禁误拦 + - `/introspect` / `/revoke` 权限模型正确 + - `/userinfo` 基于 SSO token,而不是平台 session +6. 对关键契约类 handler 增加“路由/方法/状态码固定断言” + +#### 验证 +- 受影响包 `go test -count=1` +- 必须确保断言收紧后仍稳定通过 + +--- + +### P1-3:强化 JWT secret 治理为启动硬门禁 + +#### 目标 +让 release 模式下的 JWT 配置符合项目自身文档标准。 + +#### 具体动作 +1. 明确 `config.Load()` 下的正常启动规则 +2. 在 release/standard 服务路径中强制: + - secret 缺失 → fail fast + - weak secret → fail fast +3. 保留 `LoadForBootstrap()` 仅用于初始化场景 +4. 增加配置单元测试 + +#### 验证 +- `go test ./internal/config -count=1` +- 缺失/弱 secret 场景必须失败 + +--- + +### P1-4:接通用户状态 / 权限变更后的缓存失效链路 + +#### 目标 +避免密码、状态、角色、权限变更后继续使用陈旧缓存。 + +#### 具体动作 +1. 梳理以下写路径: + - `ChangePassword` + - `UpdateStatus` + - `BatchUpdateStatus` + - `AssignRoles` + - `DeleteAdmin` + - `AssignPermissions` +2. 设计缓存失效注入方式 + - 推荐通过依赖注入引入失效能力 + - 不要让 service 直接依赖具体 middleware 实现细节 +3. 在写路径完成后主动失效: + - user_state + - user_perms + - 受影响角色下的用户权限缓存 + +#### 验证 +- 增加回归测试: + - 改密码后旧 token / 旧状态缓存失效 + - 改角色/权限后权限即时生效 + +--- + +### P1-5:清理拟真 secret 示例 + +#### 目标 +恢复文档敏感边界清洁度。 + +#### 具体动作 +1. 清理 `docs/archive/OAUTH_INTEGRATION.md` 中拟真值 +2. 全仓搜索其它类似格式示例 +3. 统一替换为显式占位符 + +#### 验证 +- 搜索确认无拟真 secret 示例残留 + +--- + +## 四、P2 修复计划(在 P0/P1 收口后处理) + +### P2-1:修复 `strconvAtoi` 吞错问题 + +#### 目标 +非法 status 参数返回显式错误,而不是静默当作 0。 + +#### 动作 +1. 修改 `internal/api/handler/export_handler.go` 中 `strconvAtoi` +2. 非数字输入返回 error +3. `ExportUsers` 中对非法 `status` 返回 400 +4. 增加回归测试 + +#### 验证 +- `status=abc` → 400 + +--- + +### P2-2:头像上传改为流式写盘 + +#### 目标 +消除不必要的整块内存分配。 + +#### 动作 +1. 用 `os.Create` + `io.Copy` 代替 `Read + WriteFile` +2. 保持现有 magic bytes 校验逻辑 +3. 确保失败时清理半成品文件 + +#### 验证 +- 头像上传相关测试通过 +- 文件写入失败场景仍能回滚 + +--- + +### P2-3:头像上传响应改为明确 struct + +#### 目标 +让返回 schema 与注释一致。 + +#### 动作 +1. 引入明确响应 struct +2. 更新 Swagger 注释 / handler 返回值 +3. 同步前端类型 + +#### 验证 +- 相关 handler test +- 前端编译通过 + +--- + +### P2-4:前端构建大 chunk 警告优化 + +#### 目标 +降低主包体积,改善生产可维护性。 + +#### 动作 +1. 识别大 chunk 页面 +2. 做路由级动态拆分 +3. 必要时拆分 antd 重型页面模块 + +#### 验证 +- `npm run build` +- 观察 chunk 体积变化 + +--- + +## 五、修复计划完整性审核 + +本节用于确认:**计划是否覆盖 review 报告中的全部问题**。 + +| Review 问题 | 计划覆盖项 | 覆盖状态 | +|---|---|---| +| Swagger 空壳 | P0-1 | 已覆盖 | +| Swagger 注释与真实路由系统性漂移 | P0-2 | 已覆盖 | +| SSO code 未绑定 redirect_uri | P0-3 | 已覆盖 | +| SSO implicit flow | P0-4 | 已覆盖 | +| SSO `/token` `/introspect` `/revoke` `/userinfo` 鉴权模型错误 | P0-5 | 已覆盖 | +| 宽松状态码测试掩盖问题 | P1-2 | 已覆盖 | +| `go vet` 不通过 | P1-1 | 已覆盖 | +| JWT secret 硬门禁不足 | P1-3 | 已覆盖 | +| 状态 / 权限缓存失效未接入 | P1-4 | 已覆盖 | +| 拟真 secret 示例 | P1-5 | 已覆盖 | +| `strconvAtoi` 吞错 | P2-1 | 已覆盖 | +| 头像整块读入内存 | P2-2 | 已覆盖 | +| 头像响应 schema 漂移 | P2-3 | 已覆盖 | + +### 审核结论 + +当前修复计划已经覆盖 review 报告中的**全部问题项**。 +其中最关键的改进是: + +- 不再把“Swagger 路由错误”视为单点问题,而是按**系统性契约漂移**处理 +- 新增 P0-5,明确修复 SSO route group / auth model 的结构性错误 + +这两点补齐后,计划才具备“能够完整修复 review 报告问题”的条件。 + +--- + +## 六、推荐执行顺序 + +### 阶段 1:协议与契约止血 +1. P0-5 修 SSO route group / auth model +2. P0-3 修 SSO code / redirect_uri 绑定 +3. P0-4 禁 implicit flow +4. P0-2 系统性修正 Swagger 注释与真实路由漂移 +5. P0-1 生成有效 Swagger + +### 阶段 2:质量门禁与测试收口 +6. P1-1 修复 `go vet` +7. P1-2 收紧 export / sso / 契约类 handler 测试 +8. P1-3 强化 JWT secret 启动门禁 + +### 阶段 3:一致性与边界治理 +9. P1-4 接通缓存失效链路 +10. P1-5 清理拟真 secret 示例 + +### 阶段 4:实现质量优化 +11. P2-1 修 status 参数吞错 +12. P2-2 头像流式写盘 +13. P2-3 头像响应 struct 化 +14. P2-4 前端 chunk 优化 + +--- + +## 七、每阶段完成后的最小验证矩阵 + +### P0 阶段后 +```bash +go test ./internal/auth ./internal/api/handler ./internal/api/router -count=1 +go build ./cmd/server +``` +并检查 Swagger 生成结果。 + +### P1 阶段后 +```bash +go vet ./... +go test ./... -count=1 +go build ./cmd/server +cd frontend/admin && env -u NODE_ENV npm run test:run +cd frontend/admin && env -u NODE_ENV npm run build +``` + +### P2 阶段后 +按受影响范围重跑: + +```bash +go test ./internal/api/handler ./internal/service ./internal/repository -count=1 +cd frontend/admin && env -u NODE_ENV npm run build +``` + +--- + +## 八、完成标准 + +只有同时满足以下条件,才能把本轮问题标记为“已收口”: + +1. SSO code flow 绑定完整,implicit flow 已禁用 +2. SSO `/token`、`/introspect`、`/revoke`、`/userinfo` 的访问控制模型正确 +3. Swagger 文档非空且关键路径正确 +4. 注释 / 路由 / 文档 / 前端 / 测试中的 API 契约一致 +5. `go vet ./...` 通过 +6. handler 关键测试不再接受互斥状态码混过 +7. JWT secret 治理与项目文档标准一致 +8. 缓存失效链路有真实接入与回归测试 +9. 状态文档与 README 只保留已验证事实 diff --git a/docs/docs.go b/docs/docs.go index d8d3ce6..722d69b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1,13 +1,8086 @@ +// Package docs Code generated by swaggo/swag. DO NOT EDIT package docs import "github.com/swaggo/swag" -type swaggerSpec struct{} +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/api/v1/admin/admins": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有管理员用户列表(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取管理员列表", + "responses": { + "200": { + "description": "管理员列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新管理员账号(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "创建管理员", + "parameters": [ + { + "description": "管理员信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CreateAdminRequest" + } + } + ], + "responses": { + "201": { + "description": "管理员创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/admins/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除管理员角色(最后管理员保护、自删保护)(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "删除管理员", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "管理员已移除", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的用户ID", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "409": { + "description": "无法删除(最后管理员或自删)", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/devices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有设备列表(仅管理员),支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取所有设备列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(游标模式)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.DeviceListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/settings": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统配置、安全设置和功能开关信息", + "produces": [ + "application/json" + ], + "tags": [ + "系统设置" + ], + "summary": "获取系统设置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.SystemSettings" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/stats/dashboard": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统仪表盘统计数据(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "统计" + ], + "summary": "获取仪表盘统计", + "responses": { + "200": { + "description": "仪表盘数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.DashboardStats" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/stats/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取用户统计数据(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "统计" + ], + "summary": "获取用户统计", + "responses": { + "200": { + "description": "用户统计数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserStats" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/users/export": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "导出用户数据为 CSV 或 Excel 格式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据导入导出" + ], + "summary": "导出用户数据", + "parameters": [ + { + "enum": [ + "csv", + "excel" + ], + "type": "string", + "default": "csv", + "description": "导出格式", + "name": "format", + "in": "query" + }, + { + "type": "string", + "description": "导出字段,逗号分隔", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "关键词过滤", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "用户状态过滤", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用户数据文件", + "schema": { + "type": "file" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/users/import": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "从 CSV 或 Excel 文件导入用户数据", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据导入导出" + ], + "summary": "导入用户数据", + "parameters": [ + { + "type": "file", + "description": "导入文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "enum": [ + "csv", + "excel" + ], + "type": "string", + "default": "csv", + "description": "文件格式", + "name": "format", + "in": "query" + } + ], + "responses": { + "200": { + "description": "导入结果", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/users/import/template": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "下载用户批量导入的 CSV 或 Excel 模板", + "produces": [ + "application/json" + ], + "tags": [ + "数据导入导出" + ], + "summary": "获取用户导入模板", + "parameters": [ + { + "enum": [ + "csv", + "excel" + ], + "type": "string", + "default": "csv", + "description": "模板格式", + "name": "format", + "in": "query" + } + ], + "responses": { + "200": { + "description": "导入模板文件", + "schema": { + "type": "file" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/disable": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "输入验证码禁用 TOTP 两步验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "禁用 TOTP 两步验证", + "parameters": [ + { + "description": "验证码", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.DisableTOTPRequest" + } + } + ], + "responses": { + "200": { + "description": "禁用成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证或验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/enable": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "输入验证码启用 TOTP 两步验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "启用 TOTP 两步验证", + "parameters": [ + { + "description": "验证码", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.EnableTOTPRequest" + } + } + ], + "responses": { + "200": { + "description": "启用成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证或验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/setup": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为当前用户设置 TOTP 两步验证,返回密钥和二维码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "设置 TOTP 两步验证", + "responses": { + "200": { + "description": "TOTP设置信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.TOTPSetupResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/status": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的TOTP两步验证状态", + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "获取TOTP状态", + "responses": { + "200": { + "description": "TOTP状态", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.TOTPStatusResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/verify": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "在登录或其他敏感操作时验证 TOTP 验证码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "验证 TOTP 验证码", + "parameters": [ + { + "description": "验证码", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.VerifyTOTPRequest" + } + } + ], + "responses": { + "200": { + "description": "验证结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.VerifyTOTPResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证或验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/activate-email": { + "post": { + "description": "使用邮箱激活token激活用户账号", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "激活用户邮箱", + "parameters": [ + { + "description": "激活请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ActivateEmailRequest" + } + } + ], + "responses": { + "200": { + "description": "激活成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "token缺失", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "token无效或已过期", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/bootstrap-admin": { + "post": { + "security": [ + { + "BootstrapSecret": [] + } + ], + "description": "在系统未配置管理员时,创建第一个管理员账号(需要BOOTSTRAP_SECRET)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "系统初始化" + ], + "summary": "引导初始化管理员账号", + "parameters": [ + { + "type": "string", + "description": "引导密钥", + "name": "X-Bootstrap-Secret", + "in": "header", + "required": true + }, + { + "description": "管理员信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.BootstrapAdminRequest" + } + } + ], + "responses": { + "201": { + "description": "管理员创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserInfo" + } + } + } + ] + } + }, + "401": { + "description": "引导密钥无效", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "引导初始化未授权", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/capabilities": { + "get": { + "description": "返回系统支持的认证方式和配置(如是否需要邮件激活、是否支持OAuth等)", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "获取系统认证能力", + "responses": { + "200": { + "description": "认证能力配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.AuthCapabilities" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/captcha": { + "get": { + "description": "生成图形验证码", + "produces": [ + "application/json" + ], + "tags": [ + "验证码" + ], + "summary": "生成验证码", + "responses": { + "200": { + "description": "验证码信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.CaptchaResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/captcha/image": { + "get": { + "description": "根据captcha_id获取验证码图片(当前未实现)", + "produces": [ + "application/json" + ], + "tags": [ + "验证码" + ], + "summary": "获取验证码图片", + "parameters": [ + { + "type": "string", + "description": "验证码ID", + "name": "captcha_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "验证码图片", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/captcha/verify": { + "post": { + "description": "验证用户输入的验证码是否正确", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "验证码" + ], + "summary": "验证验证码", + "parameters": [ + { + "description": "验证码信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.VerifyCaptchaRequest" + } + } + ], + "responses": { + "200": { + "description": "验证成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.VerifyResponse" + } + } + } + ] + } + }, + "400": { + "description": "验证码无效", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/csrf-token": { + "get": { + "description": "由于系统使用JWT Bearer Token认证,不存在CSRF风险,返回空token", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "获取CSRF令牌", + "responses": { + "200": { + "description": "CSRF token(为空)", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.CSRFTokenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/forgot-password": { + "post": { + "description": "请求密码重置邮件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "忘记密码", + "parameters": [ + { + "description": "邮箱地址", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ForgotPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码重置邮件已发送", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/forgot-password/phone": { + "post": { + "description": "向绑定的手机号发送短信验证码用于重置密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "发送短信验证码(忘记密码)", + "parameters": [ + { + "description": "手机号", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ForgotPasswordByPhoneRequest" + } + } + ], + "responses": { + "200": { + "description": "验证码发送成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信服务未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "description": "用户使用账号密码登录,支持多种认证方式(用户名/邮箱/手机号)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "认证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "429": { + "description": "登录尝试过多", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/login/code": { + "post": { + "description": "使用手机号和短信验证码登录(带设备信息以支持设备信任链路)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "短信验证" + ], + "summary": "短信验证码登录", + "parameters": [ + { + "description": "登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.SMSLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信登录未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/login/email-code": { + "post": { + "description": "使用邮箱和验证码完成登录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "邮箱验证码登录", + "parameters": [ + { + "description": "登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.LoginByEmailCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "验证码错误或已过期", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/login/totp-verify": { + "post": { + "description": "当登录返回requires_totp=true时,使用此接口完成TOTP验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "TOTP验证(密码登录后)", + "parameters": [ + { + "description": "TOTP验证请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.TOTPVerifyRequest" + } + } + ], + "responses": { + "200": { + "description": "验证成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "TOTP验证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "使当前 access_token 和 refresh_token 失效", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登出", + "parameters": [ + { + "description": "登出请求(token可从header获取)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/service.LogoutRequest" + } + } + ], + "responses": { + "200": { + "description": "登出成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/oauth/exchange": { + "post": { + "description": "使用OAuth code交换access_token(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "OAuth令牌交换", + "parameters": [ + { + "type": "string", + "description": "OAuth提供商", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OAuth未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/oauth/providers": { + "get": { + "description": "返回系统已配置并启用的OAuth提供商列表", + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "获取OAuth提供商列表", + "responses": { + "200": { + "description": "提供商列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.OAuthProvidersResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/oauth/{provider}": { + "get": { + "description": "发起OAuth登录流程(当前未配置)", + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "OAuth登录初始化", + "parameters": [ + { + "type": "string", + "description": "OAuth提供商(如 github, google)", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OAuth未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/oauth/{provider}/callback": { + "get": { + "description": "处理OAuth provider回调(当前未配置)", + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "OAuth回调处理", + "parameters": [ + { + "type": "string", + "description": "OAuth提供商", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OAuth未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/password/validate": { + "post": { + "description": "验证密码重置链接中的 Token 是否有效", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "验证密码重置 Token", + "parameters": [ + { + "description": "重置 Token", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ValidateResetTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "Token验证结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.ValidateTokenResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "使用 refresh_token 获取新的 access_token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "刷新访问令牌", + "parameters": [ + { + "description": "刷新令牌请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.RefreshTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "refresh_token无效或已过期", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "用户注册新账号,支持用户名+密码或手机号注册", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "注册请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "注册成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserInfo" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "409": { + "description": "用户已存在", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/resend-activation": { + "post": { + "description": "重新发送账号激活邮件(防枚举:无论邮箱是否注册都返回成功)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "重发激活邮件", + "parameters": [ + { + "description": "邮箱地址", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ResendActivationRequest" + } + } + ], + "responses": { + "200": { + "description": "激活邮件已发送(如果邮箱已注册)", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "邮箱格式错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/reset-password": { + "post": { + "description": "使用 Token 重置密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "重置密码", + "parameters": [ + { + "description": "重置请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码重置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/reset-password/phone": { + "post": { + "description": "使用短信验证码重置登录密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "通过短信验证码重置密码", + "parameters": [ + { + "description": "重置请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ResetPasswordByPhoneRequest" + } + } + ], + "responses": { + "200": { + "description": "密码重置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信服务未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/send-code": { + "post": { + "description": "向指定手机号发送短信验证码(用于注册或登录)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "短信验证" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "description": "发送验证码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.SendCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "发送成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信服务未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/send-email-code": { + "post": { + "description": "发送邮箱登录验证码(防枚举:无论邮箱是否注册都返回成功)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "发送邮箱验证码", + "parameters": [ + { + "description": "邮箱地址", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.SendEmailCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "验证码已发送", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "邮箱格式错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/userinfo": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取已登录用户的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "获取当前用户信息", + "responses": { + "200": { + "description": "用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserInfo" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/custom-fields": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有自定义字段定义列表", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "获取自定义字段列表", + "responses": { + "200": { + "description": "字段列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的自定义字段定义(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "创建自定义字段", + "parameters": [ + { + "description": "字段定义", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateFieldRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/custom-fields/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取自定义字段定义", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "获取自定义字段详情", + "parameters": [ + { + "type": "integer", + "description": "字段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "字段信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + ] + } + }, + "404": { + "description": "字段不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新自定义字段定义(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "更新自定义字段", + "parameters": [ + { + "type": "integer", + "description": "字段ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateFieldRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "字段不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除自定义字段定义(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "删除自定义字段", + "parameters": [ + { + "type": "integer", + "description": "字段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "字段不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的所有设备记录", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取当前用户的设备列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.DeviceListResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "当前用户创建设备记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "创建设备记录", + "parameters": [ + { + "description": "设备信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateDeviceRequest" + } + } + ], + "responses": { + "201": { + "description": "设备创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/by-device-id/{deviceId}/trust": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据设备唯一标识字符串设置设备为信任状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "根据设备标识设置信任", + "parameters": [ + { + "type": "string", + "description": "设备唯一标识", + "name": "deviceId", + "in": "path", + "required": true + }, + { + "description": "信任配置", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.TrustDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/me/logout-others": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "登出当前用户除指定设备外的所有其他设备", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "登出其他设备", + "parameters": [ + { + "type": "string", + "description": "当前设备ID", + "name": "X-Device-ID", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "登出成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的设备ID", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/me/trusted": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的信任设备列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取信任设备列表", + "responses": { + "200": { + "description": "信任设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定用户的设备列表(仅本人或管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取用户设备列表", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.DeviceListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取设备详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取设备详情", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "设备信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + ] + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新设备的基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "更新设备信息", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除设备记录", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "删除设备", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新设备状态(active/inactive)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "更新设备状态", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateDeviceStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/{id}/trust": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "将指定设备设置为信任设备,在信任期内免二次验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "设置设备为信任设备", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "信任配置", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.TrustDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "取消设备的信任状态", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "取消设备信任", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "取消成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/login": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有登录日志(仅管理员),支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取登录日志列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(游标模式)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "登录日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.LoginLogListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/login/export": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "导出登录日志为 CSV 文件", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "导出登录日志", + "parameters": [ + { + "type": "string", + "description": "开始时间", + "name": "start_time", + "in": "query" + }, + { + "type": "string", + "description": "结束时间", + "name": "end_time", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "用户ID", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "CSV文件", + "schema": { + "type": "file" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/login/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的登录日志", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取登录日志", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "登录日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.LoginLogListResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/operation": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有操作日志(仅管理员),支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取操作日志列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(游标模式)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "操作日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.OperationLogListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/operation/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的操作日志", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取操作日志", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "操作日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.OperationLogListResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/permissions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "获取权限列表", + "responses": { + "200": { + "description": "权限列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的权限定义(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "创建权限", + "parameters": [ + { + "description": "权限信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreatePermissionRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/permissions/tree": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统权限的树形结构", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "获取权限树", + "responses": { + "200": { + "description": "权限树", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/permissions/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取权限详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "获取权限详情", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "权限信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + ] + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新权限信息(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "更新权限", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdatePermissionRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除权限定义(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "删除权限", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/permissions/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新权限状态(enabled/disabled)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "更新权限状态", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdatePermissionStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统角色列表", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色列表", + "responses": { + "200": { + "description": "角色列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.RoleListResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新角色(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "角色信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateRoleRequest" + } + } + ], + "responses": { + "201": { + "description": "角色创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取角色详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色详情", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "角色信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + ] + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新角色信息(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "更新角色", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除角色(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "删除角色", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles/{id}/permissions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取角色的权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色权限列表", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "权限列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + } + ] + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为角色分配权限(替换现有权限)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "分配角色权限", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "权限ID列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.AssignPermissionsRequest" + } + } + ], + "responses": { + "200": { + "description": "权限分配成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新角色状态(enabled/disabled)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "更新角色状态", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateRoleStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/authorize": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "处理 SSO 授权请求,返回授权码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "SSO 授权", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "回调地址", + "name": "redirect_uri", + "in": "query", + "required": true + }, + { + "enum": [ + "code" + ], + "type": "string", + "description": "响应类型", + "name": "response_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "授权范围", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "状态参数", + "name": "state", + "in": "query" + } + ], + "responses": { + "302": { + "description": "重定向到回调地址", + "schema": { + "type": "string" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/introspect": { + "post": { + "description": "验证 Access Token 的有效性并返回相关信息", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "验证 Access Token", + "parameters": [ + { + "type": "string", + "description": "Access Token", + "name": "token", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端密钥", + "name": "client_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "Token信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.IntrospectResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "客户端认证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/revoke": { + "post": { + "description": "撤销指定的 Access Token", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "撤销 Access Token", + "parameters": [ + { + "type": "string", + "description": "Access Token", + "name": "token", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端密钥", + "name": "client_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "撤销成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "客户端认证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/token": { + "post": { + "description": "使用授权码获取 Access Token(授权码模式第二步)", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "获取 Access Token", + "parameters": [ + { + "enum": [ + "authorization_code" + ], + "type": "string", + "description": "授权类型", + "name": "grant_type", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "授权码", + "name": "code", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "回调地址", + "name": "redirect_uri", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端密钥", + "name": "client_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "访问令牌响应", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.TokenResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "客户端认证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/userinfo": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前通过 SSO Access Token 授权的用户信息", + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "获取 SSO 用户信息", + "responses": { + "200": { + "description": "用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserInfoResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/theme/active": { + "get": { + "description": "获取当前系统正在使用的主题(公开接口)", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取当前生效的主题", + "responses": { + "200": { + "description": "当前生效主题", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有主题(包括已禁用的)", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取所有主题", + "responses": { + "200": { + "description": "主题列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的主题配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "创建主题", + "parameters": [ + { + "description": "主题信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateThemeRequest" + } + } + ], + "responses": { + "201": { + "description": "主题创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes/default": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统默认主题", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取默认主题", + "responses": { + "200": { + "description": "默认主题", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes/default/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "将指定主题设为系统默认主题", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "设置默认主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取主题详情", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "主题详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新指定主题的配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "更新主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateThemeRequest" + } + } + ], + "responses": { + "200": { + "description": "主题更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定的主题", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "删除主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "主题删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取用户列表,支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "偏移分页偏移量", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用户列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserListResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新用户账号(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "用户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CreateUserRequest" + } + } + ], + "responses": { + "201": { + "description": "用户创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/batch": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "批量删除多个用户(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "批量删除用户", + "parameters": [ + { + "description": "批量删除请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.BatchDeleteRequest" + } + } + ], + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/batch/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "批量更新多个用户的状态(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "批量更新用户状态", + "parameters": [ + { + "description": "批量更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.BatchUpdateStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "批量更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-email": { + "post": { + "description": "使用邮箱验证码绑定账号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱绑定" + ], + "summary": "绑定邮箱", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "description": "解绑账号关联的邮箱(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱绑定" + ], + "summary": "解绑邮箱", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-email/code": { + "post": { + "description": "发送验证码到邮箱以绑定邮箱(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱绑定" + ], + "summary": "发送邮箱绑定验证码", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-phone": { + "post": { + "description": "使用手机验证码绑定账号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "手机绑定" + ], + "summary": "绑定手机号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "description": "解绑账号关联的手机号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "手机绑定" + ], + "summary": "解绑手机号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-phone/code": { + "post": { + "description": "发送验证码到手机以绑定手机号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "手机绑定" + ], + "summary": "发送手机绑定验证码", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-social": { + "post": { + "description": "绑定第三方社交账号到当前用户(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "社交账号" + ], + "summary": "绑定社交账号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-social/{provider}": { + "delete": { + "description": "解绑当前用户关联的第三方社交账号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "社交账号" + ], + "summary": "解绑社交账号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/custom-fields": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的自定义字段值", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "获取用户自定义字段值", + "responses": { + "200": { + "description": "字段值", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.CustomFieldValuesResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "设置当前用户的自定义字段值", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "设置用户自定义字段值", + "parameters": [ + { + "description": "字段值", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.SetUserFieldValuesRequest" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/social-accounts": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户绑定的第三方社交账号列表", + "produces": [ + "application/json" + ], + "tags": [ + "社交账号" + ], + "summary": "获取已绑定的社交账号列表", + "responses": { + "200": { + "description": "社交账号列表", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取用户详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户详情", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新用户的基本信息(仅管理员或本人)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "更新用户信息", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除用户账号(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/avatar": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "上传并更新用户头像(仅本人或管理员)", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户头像" + ], + "summary": "上传用户头像", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "头像文件(最大5MB,支持jpg/jpeg/png/gif/webp)", + "name": "avatar", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.AvatarResponse" + } + } + } + ] + } + }, + "400": { + "description": "文件无效或大小超限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "修改用户密码(仅管理员或本人)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "修改用户密码", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "密码信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdatePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码修改成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/roles": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定用户的角色列表(仅本人或管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户角色列表", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "角色列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为用户分配角色(替换现有角色)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "分配用户角色", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "角色ID列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.AssignRolesRequest" + } + } + ], + "responses": { + "200": { + "description": "角色分配成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新用户账号状态(active/inactive/locked/disabled)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "更新用户状态", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/webhooks": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的 Webhook 配置列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "获取 Webhook 列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Webhook列表", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的 Webhook 配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "创建 Webhook", + "parameters": [ + { + "description": "Webhook信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateWebhookRequest" + } + } + ], + "responses": { + "201": { + "description": "Webhook创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Webhook" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/webhooks/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新指定 Webhook 的配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "更新 Webhook", + "parameters": [ + { + "type": "integer", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateWebhookRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定的 Webhook 配置", + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "删除 Webhook", + "parameters": [ + { + "type": "integer", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/webhooks/{id}/deliveries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定 Webhook 的最近投递记录", + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "获取 Webhook 投递记录", + "parameters": [ + { + "type": "integer", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "返回记录数量", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "投递记录列表", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + } + }, + "definitions": { + "auth.OAuthProvider": { + "type": "string", + "enum": [ + "wechat", + "qq", + "weibo", + "google", + "facebook", + "twitter", + "github", + "alipay", + "douyin" + ], + "x-enum-varnames": [ + "OAuthProviderWeChat", + "OAuthProviderQQ", + "OAuthProviderWeibo", + "OAuthProviderGoogle", + "OAuthProviderFacebook", + "OAuthProviderTwitter", + "OAuthProviderGitHub", + "OAuthProviderAlipay", + "OAuthProviderDouyin" + ] + }, + "auth.OAuthProviderInfo": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/auth.OAuthProvider" + } + } + }, + "domain.UserStatus": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-comments": { + "UserStatusActive": "已激活", + "UserStatusDisabled": "已禁用", + "UserStatusInactive": "未激活", + "UserStatusLocked": "已锁定" + }, + "x-enum-descriptions": [ + "未激活", + "已激活", + "已锁定", + "已禁用" + ], + "x-enum-varnames": [ + "UserStatusInactive", + "UserStatusActive", + "UserStatusLocked", + "UserStatusDisabled" + ] + }, + "domain.Webhook": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "integer" + }, + "events": { + "description": "JSON 数组,订阅的事件类型", + "type": "string" + }, + "id": { + "type": "integer" + }, + "max_retries": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/domain.WebhookStatus" + }, + "timeout_sec": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "domain.WebhookEventType": { + "type": "string", + "enum": [ + "user.registered", + "user.login", + "user.logout", + "user.updated", + "user.deleted", + "user.locked", + "user.password_changed", + "user.password_reset", + "user.totp_enabled", + "user.totp_disabled", + "user.login_failed", + "security.anomaly_detected" + ], + "x-enum-varnames": [ + "EventUserRegistered", + "EventUserLogin", + "EventUserLogout", + "EventUserUpdated", + "EventUserDeleted", + "EventUserLocked", + "EventPasswordChanged", + "EventPasswordReset", + "EventTOTPEnabled", + "EventTOTPDisabled", + "EventLoginFailed", + "EventAnomalyDetected" + ] + }, + "domain.WebhookStatus": { + "type": "integer", + "enum": [ + 1, + 0 + ], + "x-enum-varnames": [ + "WebhookStatusActive", + "WebhookStatusInactive" + ] + }, + "handler.ActivateEmailRequest": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "handler.AssignPermissionsRequest": { + "type": "object", + "properties": { + "permission_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "handler.AssignRolesRequest": { + "type": "object", + "properties": { + "role_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "handler.AvatarResponse": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "thumbnail": { + "type": "string" + } + } + }, + "handler.BootstrapAdminRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.CSRFTokenResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "handler.CaptchaResponse": { + "type": "object", + "properties": { + "captcha_id": { + "type": "string" + }, + "image": { + "type": "string" + } + } + }, + "handler.CreateAdminRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.CreateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.CustomFieldValuesResponse": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "handler.DeviceListResponse": { + "type": "object", + "properties": { + "cursor": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "items": {}, + "next_cursor": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.DisableTOTPRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "handler.EnableTOTPRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "handler.ForgotPasswordByPhoneRequest": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string" + } + } + }, + "handler.ForgotPasswordRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.IntrospectResponse": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "exp": { + "type": "integer" + }, + "scope": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "handler.LoginByEmailCodeRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "handler.LoginLogListResponse": { + "type": "object", + "properties": { + "cursor": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "items": {}, + "list": {}, + "next_cursor": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.OAuthProvidersResponse": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.OperationLogListResponse": { + "type": "object", + "properties": { + "cursor": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "items": {}, + "list": {}, + "next_cursor": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.RefreshTokenRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "handler.ResendActivationRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.ResetPasswordByPhoneRequest": { + "type": "object", + "required": [ + "code", + "new_password", + "phone" + ], + "properties": { + "code": { + "type": "string" + }, + "new_password": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "handler.ResetPasswordRequest": { + "type": "object", + "properties": { + "new_password": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "handler.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "handler.RoleListResponse": { + "type": "object", + "properties": { + "items": {}, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.SMSLoginRequest": { + "type": "object", + "required": [ + "code", + "phone" + ], + "properties": { + "code": { + "type": "string" + }, + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "handler.SendEmailCodeRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.SetUserFieldValuesRequest": { + "type": "object", + "properties": { + "values": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "handler.SwaggerCustomField": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "field_key": { + "type": "string" + }, + "field_type": { + "type": "string" + }, + "help_text": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "options": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.SwaggerDevice": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "current": { + "type": "boolean" + }, + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "device_type": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "is_trusted": { + "type": "boolean" + }, + "last_active_at": { + "type": "string" + }, + "last_used_at": { + "type": "string" + }, + "location": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "trusted_until": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "handler.SwaggerPermission": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.SwaggerRole": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_system": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.SwaggerTheme": { + "type": "object", + "properties": { + "accent_color": { + "type": "string" + }, + "background_color": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "error_color": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "info_color": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "primary_color": { + "type": "string" + }, + "secondary_color": { + "type": "string" + }, + "success_color": { + "type": "string" + }, + "text_color": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "warning_color": { + "type": "string" + } + } + }, + "handler.TOTPSetupResponse": { + "type": "object", + "properties": { + "qr_code_base64": { + "type": "string" + }, + "recovery_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "secret": { + "type": "string" + } + } + }, + "handler.TOTPStatusResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "handler.TOTPVerifyRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "temp_token": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "handler.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "handler.TrustDeviceRequest": { + "type": "object", + "properties": { + "trust_duration": { + "description": "信任持续时间,如 \"30d\" 表示30天", + "type": "string" + } + } + }, + "handler.UpdateDeviceStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdatePasswordRequest": { + "type": "object", + "properties": { + "new_password": { + "type": "string" + }, + "old_password": { + "type": "string" + } + } + }, + "handler.UpdatePermissionStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdateRoleStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdateStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + } + } + }, + "handler.UserInfoResponse": { + "type": "object", + "properties": { + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "handler.UserListResponse": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "items": {}, + "limit": { + "type": "integer" + }, + "next_cursor": { + "type": "string" + }, + "offset": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "users": {} + } + }, + "handler.UserResponse": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "status": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.ValidateResetTokenRequest": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "handler.ValidateTokenResponse": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + } + } + }, + "handler.VerifyCaptchaRequest": { + "type": "object", + "properties": { + "answer": { + "type": "string" + }, + "captcha_id": { + "type": "string" + } + } + }, + "handler.VerifyResponse": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + } + } + }, + "handler.VerifyTOTPRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "device_id": { + "type": "string" + } + } + }, + "handler.VerifyTOTPResponse": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + } + } + }, + "service.AuthCapabilities": { + "type": "object", + "properties": { + "admin_bootstrap_required": { + "type": "boolean" + }, + "email_activation": { + "type": "boolean" + }, + "email_code": { + "type": "boolean" + }, + "oauth_providers": { + "type": "array", + "items": { + "$ref": "#/definitions/auth.OAuthProviderInfo" + } + }, + "password": { + "type": "boolean" + }, + "password_reset": { + "type": "boolean" + }, + "sms_code": { + "type": "boolean" + } + } + }, + "service.BatchDeleteRequest": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + } + } + }, + "service.BatchUpdateStatusRequest": { + "type": "object", + "required": [ + "ids", + "status" + ], + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + } + } + }, + "service.CreateDeviceRequest": { + "type": "object", + "required": [ + "device_id" + ], + "properties": { + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "device_type": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "service.CreateFieldRequest": { + "type": "object", + "required": [ + "field_key", + "name", + "type" + ], + "properties": { + "default": { + "type": "string" + }, + "field_key": { + "type": "string" + }, + "max_len": { + "type": "integer" + }, + "max_val": { + "type": "number" + }, + "min_len": { + "type": "integer" + }, + "min_val": { + "type": "number" + }, + "name": { + "type": "string" + }, + "options": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "sort": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.CreatePermissionRequest": { + "type": "object", + "required": [ + "code", + "name", + "type" + ], + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.CreateRoleRequest": { + "type": "object", + "required": [ + "code", + "name" + ], + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + } + } + }, + "service.CreateThemeRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "background_color": { + "type": "string" + }, + "custom_css": { + "type": "string" + }, + "custom_js": { + "type": "string" + }, + "favicon_url": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "logo_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary_color": { + "type": "string" + }, + "secondary_color": { + "type": "string" + }, + "text_color": { + "type": "string" + } + } + }, + "service.CreateWebhookRequest": { + "type": "object", + "required": [ + "events", + "name", + "url" + ], + "properties": { + "events": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/domain.WebhookEventType" + } + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "service.DashboardStats": { + "type": "object", + "properties": { + "logins": { + "$ref": "#/definitions/service.LoginStats" + }, + "users": { + "$ref": "#/definitions/service.UserStats" + } + } + }, + "service.FeaturesInfo": { + "type": "object", + "properties": { + "data_export_enabled": { + "type": "boolean" + }, + "data_import_enabled": { + "type": "boolean" + }, + "email_verification": { + "type": "boolean" + }, + "login_log_enabled": { + "type": "boolean" + }, + "oauth_providers": { + "type": "array", + "items": { + "type": "string" + } + }, + "operation_log_enabled": { + "type": "boolean" + }, + "phone_verification": { + "type": "boolean" + }, + "sso_enabled": { + "type": "boolean" + } + } + }, + "service.LoginRequest": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "device_browser": { + "description": "浏览器", + "type": "string" + }, + "device_id": { + "description": "设备唯一标识", + "type": "string" + }, + "device_name": { + "description": "设备名称", + "type": "string" + }, + "device_os": { + "description": "操作系统", + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "remember": { + "description": "记住登录", + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "service.LoginResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "requires_totp": { + "description": "RequiresTOTP 指示登录需要额外的TOTP验证(当设备未信任时)", + "type": "boolean" + }, + "temp_token": { + "description": "TempToken 临时令牌,用于TOTP验证阶段(短生命周期,不可用于常规API)", + "type": "string" + }, + "user": { + "$ref": "#/definitions/service.UserInfo" + }, + "user_id": { + "description": "UserID 当RequiresTOTP为true时返回,用于后续TOTP验证", + "type": "integer" + } + } + }, + "service.LoginStats": { + "type": "object", + "properties": { + "logins_today_failed": { + "type": "integer" + }, + "logins_today_success": { + "type": "integer" + }, + "logins_week": { + "type": "integer" + } + } + }, + "service.LogoutRequest": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "service.RegisterRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "phone_code": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service.SecurityInfo": { + "type": "object", + "properties": { + "device_trust_duration": { + "description": "秒", + "type": "integer" + }, + "login_fail_duration": { + "description": "分钟", + "type": "integer" + }, + "login_fail_lock": { + "type": "boolean" + }, + "login_fail_threshold": { + "type": "integer" + }, + "password_history": { + "type": "integer" + }, + "password_min_length": { + "type": "integer" + }, + "password_require_lowercase": { + "type": "boolean" + }, + "password_require_numbers": { + "type": "boolean" + }, + "password_require_symbols": { + "type": "boolean" + }, + "password_require_uppercase": { + "type": "boolean" + }, + "session_timeout": { + "description": "秒", + "type": "integer" + }, + "totp_enabled": { + "type": "boolean" + } + } + }, + "service.SendCodeRequest": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "scene": { + "type": "string" + } + } + }, + "service.SystemInfo": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "service.SystemSettings": { + "type": "object", + "properties": { + "features": { + "$ref": "#/definitions/service.FeaturesInfo" + }, + "security": { + "$ref": "#/definitions/service.SecurityInfo" + }, + "system": { + "$ref": "#/definitions/service.SystemInfo" + } + } + }, + "service.UpdateDeviceRequest": { + "type": "object", + "properties": { + "device_browser": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "device_type": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "location": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "service.UpdateFieldRequest": { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "max_len": { + "type": "integer" + }, + "max_val": { + "type": "number" + }, + "min_len": { + "type": "integer" + }, + "min_val": { + "type": "number" + }, + "name": { + "type": "string" + }, + "options": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "sort": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.UpdatePermissionRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "service.UpdateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + } + } + }, + "service.UpdateThemeRequest": { + "type": "object", + "properties": { + "background_color": { + "type": "string" + }, + "custom_css": { + "type": "string" + }, + "custom_js": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "favicon_url": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "logo_url": { + "type": "string" + }, + "primary_color": { + "type": "string" + }, + "secondary_color": { + "type": "string" + }, + "text_color": { + "type": "string" + } + } + }, + "service.UpdateWebhookRequest": { + "type": "object", + "properties": { + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.WebhookEventType" + } + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/domain.WebhookStatus" + }, + "url": { + "type": "string" + } + } + }, + "service.UserInfo": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "username": { + "type": "string" + } + } + }, + "service.UserStats": { + "type": "object", + "properties": { + "active_users": { + "type": "integer" + }, + "disabled_users": { + "type": "integer" + }, + "inactive_users": { + "type": "integer" + }, + "locked_users": { + "type": "integer" + }, + "new_users_month": { + "type": "integer" + }, + "new_users_today": { + "type": "integer" + }, + "new_users_week": { + "type": "integer" + }, + "total_users": { + "type": "integer" + } + } + } + } +}` -func (swaggerSpec) ReadDoc() string { - return SwaggerJSON +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "1.0", + Host: "", + BasePath: "/api/v1", + Schemes: []string{"http", "https"}, + Title: "User Management System API", + Description: "API for user management, authentication, authorization, and administration.", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, + LeftDelim: "{{", + RightDelim: "}}", } func init() { - swag.Register(swag.Name, swaggerSpec{}) + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) } diff --git a/docs/swagger.go b/docs/swagger.go index 139549a..5604656 100644 --- a/docs/swagger.go +++ b/docs/swagger.go @@ -1,50 +1 @@ -// Package docs GENERATED BY SWAG; DO NOT EDIT package docs - -import ( - "encoding/json" -) - -// SwaggerInfo holds the Swagger information -var SwaggerInfo = &swaggerInfo{ - Version: "1.0", - Host: "localhost:8080", - BasePath: "/", - Schemes: []string{"http", "https"}, - Title: "User Management System API", - Description: "API for user management, authentication, and authorization", -} - -type swaggerInfo struct { - Version string `json:"version"` - Host string `json:"host"` - BasePath string `json:"basePath"` - Schemes []string `json:"schemes"` - Title string `json:"title"` - Description string `json:"description"` -} - -// SwaggerJSON returns the swagger spec as JSON -var SwaggerJSON = `{ - "swagger": "2.0", - "info": { - "title": "User Management System API", - "description": "API for user management, authentication, and authorization", - "version": "1.0" - }, - "host": "localhost:8080", - "basePath": "/", - "schemes": ["http", "https"], - "paths": {} -}` - -// GetSwagger returns the swagger specification -func GetSwagger() []byte { - return []byte(SwaggerJSON) -} - -func init() { - // Initialize swagger - s := GetSwagger() - var _ = json.Unmarshal(s, &swaggerInfo{}) -} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..a30b67b --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,8065 @@ +{ + "schemes": [ + "http", + "https" + ], + "swagger": "2.0", + "info": { + "description": "API for user management, authentication, authorization, and administration.", + "title": "User Management System API", + "contact": {}, + "version": "1.0" + }, + "basePath": "/api/v1", + "paths": { + "/api/v1/admin/admins": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有管理员用户列表(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取管理员列表", + "responses": { + "200": { + "description": "管理员列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新管理员账号(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "创建管理员", + "parameters": [ + { + "description": "管理员信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CreateAdminRequest" + } + } + ], + "responses": { + "201": { + "description": "管理员创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/admins/{id}": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除管理员角色(最后管理员保护、自删保护)(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "删除管理员", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "管理员已移除", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的用户ID", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "409": { + "description": "无法删除(最后管理员或自删)", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/devices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有设备列表(仅管理员),支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取所有设备列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(游标模式)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.DeviceListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/settings": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统配置、安全设置和功能开关信息", + "produces": [ + "application/json" + ], + "tags": [ + "系统设置" + ], + "summary": "获取系统设置", + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.SystemSettings" + } + } + } + ] + } + } + } + } + }, + "/api/v1/admin/stats/dashboard": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统仪表盘统计数据(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "统计" + ], + "summary": "获取仪表盘统计", + "responses": { + "200": { + "description": "仪表盘数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.DashboardStats" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/stats/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取用户统计数据(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "统计" + ], + "summary": "获取用户统计", + "responses": { + "200": { + "description": "用户统计数据", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserStats" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/users/export": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "导出用户数据为 CSV 或 Excel 格式", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据导入导出" + ], + "summary": "导出用户数据", + "parameters": [ + { + "enum": [ + "csv", + "excel" + ], + "type": "string", + "default": "csv", + "description": "导出格式", + "name": "format", + "in": "query" + }, + { + "type": "string", + "description": "导出字段,逗号分隔", + "name": "fields", + "in": "query" + }, + { + "type": "string", + "description": "关键词过滤", + "name": "keyword", + "in": "query" + }, + { + "type": "integer", + "description": "用户状态过滤", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用户数据文件", + "schema": { + "type": "file" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/users/import": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "从 CSV 或 Excel 文件导入用户数据", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "数据导入导出" + ], + "summary": "导入用户数据", + "parameters": [ + { + "type": "file", + "description": "导入文件", + "name": "file", + "in": "formData", + "required": true + }, + { + "enum": [ + "csv", + "excel" + ], + "type": "string", + "default": "csv", + "description": "文件格式", + "name": "format", + "in": "query" + } + ], + "responses": { + "200": { + "description": "导入结果", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/admin/users/import/template": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "下载用户批量导入的 CSV 或 Excel 模板", + "produces": [ + "application/json" + ], + "tags": [ + "数据导入导出" + ], + "summary": "获取用户导入模板", + "parameters": [ + { + "enum": [ + "csv", + "excel" + ], + "type": "string", + "default": "csv", + "description": "模板格式", + "name": "format", + "in": "query" + } + ], + "responses": { + "200": { + "description": "导入模板文件", + "schema": { + "type": "file" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/disable": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "输入验证码禁用 TOTP 两步验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "禁用 TOTP 两步验证", + "parameters": [ + { + "description": "验证码", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.DisableTOTPRequest" + } + } + ], + "responses": { + "200": { + "description": "禁用成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证或验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/enable": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "输入验证码启用 TOTP 两步验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "启用 TOTP 两步验证", + "parameters": [ + { + "description": "验证码", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.EnableTOTPRequest" + } + } + ], + "responses": { + "200": { + "description": "启用成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证或验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/setup": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为当前用户设置 TOTP 两步验证,返回密钥和二维码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "设置 TOTP 两步验证", + "responses": { + "200": { + "description": "TOTP设置信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.TOTPSetupResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/status": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的TOTP两步验证状态", + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "获取TOTP状态", + "responses": { + "200": { + "description": "TOTP状态", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.TOTPStatusResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/2fa/verify": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "在登录或其他敏感操作时验证 TOTP 验证码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "两步验证" + ], + "summary": "验证 TOTP 验证码", + "parameters": [ + { + "description": "验证码", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.VerifyTOTPRequest" + } + } + ], + "responses": { + "200": { + "description": "验证结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.VerifyTOTPResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证或验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/activate-email": { + "post": { + "description": "使用邮箱激活token激活用户账号", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "激活用户邮箱", + "parameters": [ + { + "description": "激活请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ActivateEmailRequest" + } + } + ], + "responses": { + "200": { + "description": "激活成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "token缺失", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "token无效或已过期", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/bootstrap-admin": { + "post": { + "security": [ + { + "BootstrapSecret": [] + } + ], + "description": "在系统未配置管理员时,创建第一个管理员账号(需要BOOTSTRAP_SECRET)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "系统初始化" + ], + "summary": "引导初始化管理员账号", + "parameters": [ + { + "type": "string", + "description": "引导密钥", + "name": "X-Bootstrap-Secret", + "in": "header", + "required": true + }, + { + "description": "管理员信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.BootstrapAdminRequest" + } + } + ], + "responses": { + "201": { + "description": "管理员创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserInfo" + } + } + } + ] + } + }, + "401": { + "description": "引导密钥无效", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "引导初始化未授权", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/capabilities": { + "get": { + "description": "返回系统支持的认证方式和配置(如是否需要邮件激活、是否支持OAuth等)", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "获取系统认证能力", + "responses": { + "200": { + "description": "认证能力配置", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.AuthCapabilities" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/captcha": { + "get": { + "description": "生成图形验证码", + "produces": [ + "application/json" + ], + "tags": [ + "验证码" + ], + "summary": "生成验证码", + "responses": { + "200": { + "description": "验证码信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.CaptchaResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/captcha/image": { + "get": { + "description": "根据captcha_id获取验证码图片(当前未实现)", + "produces": [ + "application/json" + ], + "tags": [ + "验证码" + ], + "summary": "获取验证码图片", + "parameters": [ + { + "type": "string", + "description": "验证码ID", + "name": "captcha_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "验证码图片", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/captcha/verify": { + "post": { + "description": "验证用户输入的验证码是否正确", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "验证码" + ], + "summary": "验证验证码", + "parameters": [ + { + "description": "验证码信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.VerifyCaptchaRequest" + } + } + ], + "responses": { + "200": { + "description": "验证成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.VerifyResponse" + } + } + } + ] + } + }, + "400": { + "description": "验证码无效", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/csrf-token": { + "get": { + "description": "由于系统使用JWT Bearer Token认证,不存在CSRF风险,返回空token", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "获取CSRF令牌", + "responses": { + "200": { + "description": "CSRF token(为空)", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.CSRFTokenResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/forgot-password": { + "post": { + "description": "请求密码重置邮件", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "忘记密码", + "parameters": [ + { + "description": "邮箱地址", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ForgotPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码重置邮件已发送", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/forgot-password/phone": { + "post": { + "description": "向绑定的手机号发送短信验证码用于重置密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "发送短信验证码(忘记密码)", + "parameters": [ + { + "description": "手机号", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ForgotPasswordByPhoneRequest" + } + } + ], + "responses": { + "200": { + "description": "验证码发送成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信服务未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/login": { + "post": { + "description": "用户使用账号密码登录,支持多种认证方式(用户名/邮箱/手机号)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登录", + "parameters": [ + { + "description": "登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.LoginRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "认证失败", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "429": { + "description": "登录尝试过多", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/login/code": { + "post": { + "description": "使用手机号和短信验证码登录(带设备信息以支持设备信任链路)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "短信验证" + ], + "summary": "短信验证码登录", + "parameters": [ + { + "description": "登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.SMSLoginRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信登录未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/login/email-code": { + "post": { + "description": "使用邮箱和验证码完成登录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "邮箱验证码登录", + "parameters": [ + { + "description": "登录请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.LoginByEmailCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "登录成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "验证码错误或已过期", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/login/totp-verify": { + "post": { + "description": "当登录返回requires_totp=true时,使用此接口完成TOTP验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "TOTP验证(密码登录后)", + "parameters": [ + { + "description": "TOTP验证请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.TOTPVerifyRequest" + } + } + ], + "responses": { + "200": { + "description": "验证成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "TOTP验证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/logout": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "使当前 access_token 和 refresh_token 失效", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户登出", + "parameters": [ + { + "description": "登出请求(token可从header获取)", + "name": "request", + "in": "body", + "schema": { + "$ref": "#/definitions/service.LogoutRequest" + } + } + ], + "responses": { + "200": { + "description": "登出成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/oauth/exchange": { + "post": { + "description": "使用OAuth code交换access_token(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "OAuth令牌交换", + "parameters": [ + { + "type": "string", + "description": "OAuth提供商", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OAuth未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/oauth/providers": { + "get": { + "description": "返回系统已配置并启用的OAuth提供商列表", + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "获取OAuth提供商列表", + "responses": { + "200": { + "description": "提供商列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.OAuthProvidersResponse" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/oauth/{provider}": { + "get": { + "description": "发起OAuth登录流程(当前未配置)", + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "OAuth登录初始化", + "parameters": [ + { + "type": "string", + "description": "OAuth提供商(如 github, google)", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OAuth未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/oauth/{provider}/callback": { + "get": { + "description": "处理OAuth provider回调(当前未配置)", + "produces": [ + "application/json" + ], + "tags": [ + "OAuth" + ], + "summary": "OAuth回调处理", + "parameters": [ + { + "type": "string", + "description": "OAuth提供商", + "name": "provider", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OAuth未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/password/validate": { + "post": { + "description": "验证密码重置链接中的 Token 是否有效", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "验证密码重置 Token", + "parameters": [ + { + "description": "重置 Token", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ValidateResetTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "Token验证结果", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.ValidateTokenResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/refresh": { + "post": { + "description": "使用 refresh_token 获取新的 access_token", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "刷新访问令牌", + "parameters": [ + { + "description": "刷新令牌请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.RefreshTokenRequest" + } + } + ], + "responses": { + "200": { + "description": "刷新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.LoginResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "401": { + "description": "refresh_token无效或已过期", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/register": { + "post": { + "description": "用户注册新账号,支持用户名+密码或手机号注册", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "用户注册", + "parameters": [ + { + "description": "注册请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.RegisterRequest" + } + } + ], + "responses": { + "201": { + "description": "注册成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserInfo" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + }, + "409": { + "description": "用户已存在", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/auth/resend-activation": { + "post": { + "description": "重新发送账号激活邮件(防枚举:无论邮箱是否注册都返回成功)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "重发激活邮件", + "parameters": [ + { + "description": "邮箱地址", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ResendActivationRequest" + } + } + ], + "responses": { + "200": { + "description": "激活邮件已发送(如果邮箱已注册)", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "邮箱格式错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/reset-password": { + "post": { + "description": "使用 Token 重置密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "重置密码", + "parameters": [ + { + "description": "重置请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ResetPasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码重置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/reset-password/phone": { + "post": { + "description": "使用短信验证码重置登录密码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "密码重置" + ], + "summary": "通过短信验证码重置密码", + "parameters": [ + { + "description": "重置请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.ResetPasswordByPhoneRequest" + } + } + ], + "responses": { + "200": { + "description": "密码重置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "验证码错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信服务未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/send-code": { + "post": { + "description": "向指定手机号发送短信验证码(用于注册或登录)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "短信验证" + ], + "summary": "发送短信验证码", + "parameters": [ + { + "description": "发送验证码请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.SendCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "发送成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "503": { + "description": "短信服务未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/send-email-code": { + "post": { + "description": "发送邮箱登录验证码(防枚举:无论邮箱是否注册都返回成功)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱认证" + ], + "summary": "发送邮箱验证码", + "parameters": [ + { + "description": "邮箱地址", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.SendEmailCodeRequest" + } + } + ], + "responses": { + "200": { + "description": "验证码已发送", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "邮箱格式错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/auth/userinfo": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取已登录用户的详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "认证" + ], + "summary": "获取当前用户信息", + "responses": { + "200": { + "description": "用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/service.UserInfo" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "message": { + "type": "string" + } + } + } + ] + } + } + } + } + }, + "/api/v1/custom-fields": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有自定义字段定义列表", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "获取自定义字段列表", + "responses": { + "200": { + "description": "字段列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的自定义字段定义(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "创建自定义字段", + "parameters": [ + { + "description": "字段定义", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateFieldRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/custom-fields/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取自定义字段定义", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "获取自定义字段详情", + "parameters": [ + { + "type": "integer", + "description": "字段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "字段信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + ] + } + }, + "404": { + "description": "字段不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新自定义字段定义(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "更新自定义字段", + "parameters": [ + { + "type": "integer", + "description": "字段ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateFieldRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerCustomField" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "字段不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除自定义字段定义(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "删除自定义字段", + "parameters": [ + { + "type": "integer", + "description": "字段ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "字段不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的所有设备记录", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取当前用户的设备列表", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.DeviceListResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "当前用户创建设备记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "创建设备记录", + "parameters": [ + { + "description": "设备信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateDeviceRequest" + } + } + ], + "responses": { + "201": { + "description": "设备创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/by-device-id/{deviceId}/trust": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据设备唯一标识字符串设置设备为信任状态", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "根据设备标识设置信任", + "parameters": [ + { + "type": "string", + "description": "设备唯一标识", + "name": "deviceId", + "in": "path", + "required": true + }, + { + "description": "信任配置", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.TrustDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/me/logout-others": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "登出当前用户除指定设备外的所有其他设备", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "登出其他设备", + "parameters": [ + { + "type": "string", + "description": "当前设备ID", + "name": "X-Device-ID", + "in": "header", + "required": true + } + ], + "responses": { + "200": { + "description": "登出成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的设备ID", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/me/trusted": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的信任设备列表", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取信任设备列表", + "responses": { + "200": { + "description": "信任设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定用户的设备列表(仅本人或管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取用户设备列表", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "设备列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.DeviceListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取设备详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "获取设备详情", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "设备信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + ] + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新设备的基本信息", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "更新设备信息", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerDevice" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除设备记录", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "删除设备", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新设备状态(active/inactive)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "更新设备状态", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateDeviceStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/devices/{id}/trust": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "将指定设备设置为信任设备,在信任期内免二次验证", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "设置设备为信任设备", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "信任配置", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.TrustDeviceRequest" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "取消设备的信任状态", + "produces": [ + "application/json" + ], + "tags": [ + "设备管理" + ], + "summary": "取消设备信任", + "parameters": [ + { + "type": "integer", + "description": "设备ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "取消成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "设备不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/login": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有登录日志(仅管理员),支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取登录日志列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(游标模式)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "登录日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.LoginLogListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/login/export": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "导出登录日志为 CSV 文件", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "导出登录日志", + "parameters": [ + { + "type": "string", + "description": "开始时间", + "name": "start_time", + "in": "query" + }, + { + "type": "string", + "description": "结束时间", + "name": "end_time", + "in": "query" + }, + { + "type": "integer", + "format": "int64", + "description": "用户ID", + "name": "user_id", + "in": "query" + } + ], + "responses": { + "200": { + "description": "CSV文件", + "schema": { + "type": "file" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/login/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的登录日志", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取登录日志", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "登录日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.LoginLogListResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/operation": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有操作日志(仅管理员),支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取操作日志列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量(游标模式)", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "操作日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.OperationLogListResponse" + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/logs/operation/me": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的操作日志", + "produces": [ + "application/json" + ], + "tags": [ + "日志" + ], + "summary": "获取操作日志", + "parameters": [ + { + "type": "integer", + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "操作日志列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.OperationLogListResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/permissions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "获取权限列表", + "responses": { + "200": { + "description": "权限列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的权限定义(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "创建权限", + "parameters": [ + { + "description": "权限信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreatePermissionRequest" + } + } + ], + "responses": { + "201": { + "description": "创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/permissions/tree": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统权限的树形结构", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "获取权限树", + "responses": { + "200": { + "description": "权限树", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + } + ] + } + } + } + } + }, + "/api/v1/permissions/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取权限详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "获取权限详情", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "权限信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + ] + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新权限信息(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "更新权限", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdatePermissionRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除权限定义(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "删除权限", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/permissions/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新权限状态(enabled/disabled)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "权限管理" + ], + "summary": "更新权限状态", + "parameters": [ + { + "type": "integer", + "description": "权限ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdatePermissionStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "权限不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统角色列表", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色列表", + "responses": { + "200": { + "description": "角色列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.RoleListResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新角色(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "创建角色", + "parameters": [ + { + "description": "角色信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateRoleRequest" + } + } + ], + "responses": { + "201": { + "description": "角色创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取角色详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色详情", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "角色信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + ] + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新角色信息(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "更新角色", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateRoleRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除角色(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "删除角色", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles/{id}/permissions": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取角色的权限列表", + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "获取角色权限列表", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "权限列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerPermission" + } + } + } + } + ] + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为角色分配权限(替换现有权限)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "分配角色权限", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "权限ID列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.AssignPermissionsRequest" + } + } + ], + "responses": { + "200": { + "description": "权限分配成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/roles/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新角色状态(enabled/disabled)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "角色管理" + ], + "summary": "更新角色状态", + "parameters": [ + { + "type": "integer", + "description": "角色ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateRoleStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "角色不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/authorize": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "处理 SSO 授权请求,返回授权码", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "SSO 授权", + "parameters": [ + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "回调地址", + "name": "redirect_uri", + "in": "query", + "required": true + }, + { + "enum": [ + "code" + ], + "type": "string", + "description": "响应类型", + "name": "response_type", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "授权范围", + "name": "scope", + "in": "query" + }, + { + "type": "string", + "description": "状态参数", + "name": "state", + "in": "query" + } + ], + "responses": { + "302": { + "description": "重定向到回调地址", + "schema": { + "type": "string" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/introspect": { + "post": { + "description": "验证 Access Token 的有效性并返回相关信息", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "验证 Access Token", + "parameters": [ + { + "type": "string", + "description": "Access Token", + "name": "token", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端密钥", + "name": "client_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "Token信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.IntrospectResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "客户端认证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/revoke": { + "post": { + "description": "撤销指定的 Access Token", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "撤销 Access Token", + "parameters": [ + { + "type": "string", + "description": "Access Token", + "name": "token", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端密钥", + "name": "client_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "撤销成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "客户端认证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/token": { + "post": { + "description": "使用授权码获取 Access Token(授权码模式第二步)", + "consumes": [ + "application/x-www-form-urlencoded" + ], + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "获取 Access Token", + "parameters": [ + { + "enum": [ + "authorization_code" + ], + "type": "string", + "description": "授权类型", + "name": "grant_type", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "授权码", + "name": "code", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "回调地址", + "name": "redirect_uri", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端ID", + "name": "client_id", + "in": "formData", + "required": true + }, + { + "type": "string", + "description": "客户端密钥", + "name": "client_secret", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "访问令牌响应", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.TokenResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "客户端认证失败", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/sso/userinfo": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前通过 SSO Access Token 授权的用户信息", + "produces": [ + "application/json" + ], + "tags": [ + "SSO" + ], + "summary": "获取 SSO 用户信息", + "responses": { + "200": { + "description": "用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserInfoResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/theme/active": { + "get": { + "description": "获取当前系统正在使用的主题(公开接口)", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取当前生效的主题", + "responses": { + "200": { + "description": "当前生效主题", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取所有主题(包括已禁用的)", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取所有主题", + "responses": { + "200": { + "description": "主题列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的主题配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "创建主题", + "parameters": [ + { + "description": "主题信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateThemeRequest" + } + } + ], + "responses": { + "201": { + "description": "主题创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes/default": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取系统默认主题", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取默认主题", + "responses": { + "200": { + "description": "默认主题", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes/default/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "将指定主题设为系统默认主题", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "设置默认主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/themes/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取主题详情", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "获取主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "主题详情", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新指定主题的配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "更新主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateThemeRequest" + } + } + ], + "responses": { + "200": { + "description": "主题更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.SwaggerTheme" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定的主题", + "produces": [ + "application/json" + ], + "tags": [ + "主题管理" + ], + "summary": "删除主题", + "parameters": [ + { + "type": "integer", + "description": "主题ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "主题删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取用户列表,支持游标分页和偏移分页", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户列表", + "parameters": [ + { + "type": "string", + "description": "游标分页游标", + "name": "cursor", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "size", + "in": "query" + }, + { + "type": "integer", + "description": "偏移分页偏移量", + "name": "offset", + "in": "query" + }, + { + "type": "integer", + "description": "每页大小", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "用户列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserListResponse" + } + } + } + ] + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新用户账号(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "创建用户", + "parameters": [ + { + "description": "用户信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.CreateUserRequest" + } + } + ], + "responses": { + "201": { + "description": "用户创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/batch": { + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "批量删除多个用户(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "批量删除用户", + "parameters": [ + { + "description": "批量删除请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.BatchDeleteRequest" + } + } + ], + "responses": { + "200": { + "description": "批量删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/batch/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "批量更新多个用户的状态(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "批量更新用户状态", + "parameters": [ + { + "description": "批量更新请求", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.BatchUpdateStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "批量更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-email": { + "post": { + "description": "使用邮箱验证码绑定账号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱绑定" + ], + "summary": "绑定邮箱", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "description": "解绑账号关联的邮箱(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱绑定" + ], + "summary": "解绑邮箱", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-email/code": { + "post": { + "description": "发送验证码到邮箱以绑定邮箱(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "邮箱绑定" + ], + "summary": "发送邮箱绑定验证码", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-phone": { + "post": { + "description": "使用手机验证码绑定账号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "手机绑定" + ], + "summary": "绑定手机号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "description": "解绑账号关联的手机号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "手机绑定" + ], + "summary": "解绑手机号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-phone/code": { + "post": { + "description": "发送验证码到手机以绑定手机号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "手机绑定" + ], + "summary": "发送手机绑定验证码", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-social": { + "post": { + "description": "绑定第三方社交账号到当前用户(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "社交账号" + ], + "summary": "绑定社交账号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/bind-social/{provider}": { + "delete": { + "description": "解绑当前用户关联的第三方社交账号(当前未配置)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "社交账号" + ], + "summary": "解绑社交账号", + "responses": { + "200": { + "description": "功能未配置", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/custom-fields": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的自定义字段值", + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "获取用户自定义字段值", + "responses": { + "200": { + "description": "字段值", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.CustomFieldValuesResponse" + } + } + } + ] + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "设置当前用户的自定义字段值", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "自定义字段" + ], + "summary": "设置用户自定义字段值", + "parameters": [ + { + "description": "字段值", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.SetUserFieldValuesRequest" + } + } + ], + "responses": { + "200": { + "description": "设置成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/me/social-accounts": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户绑定的第三方社交账号列表", + "produces": [ + "application/json" + ], + "tags": [ + "社交账号" + ], + "summary": "获取已绑定的社交账号列表", + "responses": { + "200": { + "description": "社交账号列表", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "根据ID获取用户详细信息", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户详情", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "用户信息", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新用户的基本信息(仅管理员或本人)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "更新用户信息", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateUserRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.UserResponse" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除用户账号(仅管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "删除用户", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/avatar": { + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "上传并更新用户头像(仅本人或管理员)", + "consumes": [ + "multipart/form-data" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户头像" + ], + "summary": "上传用户头像", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "file", + "description": "头像文件(最大5MB,支持jpg/jpeg/png/gif/webp)", + "name": "avatar", + "in": "formData", + "required": true + } + ], + "responses": { + "200": { + "description": "上传成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/handler.AvatarResponse" + } + } + } + ] + } + }, + "400": { + "description": "文件无效或大小超限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/password": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "修改用户密码(仅管理员或本人)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "修改用户密码", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "密码信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdatePasswordRequest" + } + } + ], + "responses": { + "200": { + "description": "密码修改成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/roles": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定用户的角色列表(仅本人或管理员)", + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "获取用户角色列表", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "角色列表", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/handler.SwaggerRole" + } + } + } + } + ] + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "为用户分配角色(替换现有角色)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "分配用户角色", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "角色ID列表", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.AssignRolesRequest" + } + } + ], + "responses": { + "200": { + "description": "角色分配成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/users/{id}/status": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新用户账号状态(active/inactive/locked/disabled)(仅管理员)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "用户管理" + ], + "summary": "更新用户状态", + "parameters": [ + { + "type": "integer", + "description": "用户ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "状态信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/handler.UpdateStatusRequest" + } + } + ], + "responses": { + "200": { + "description": "状态更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "无效的状态值", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "403": { + "description": "无权限", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "404": { + "description": "用户不存在", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/webhooks": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取当前用户的 Webhook 配置列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "获取 Webhook 列表", + "parameters": [ + { + "type": "integer", + "default": 1, + "description": "页码", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 20, + "description": "每页数量", + "name": "page_size", + "in": "query" + } + ], + "responses": { + "200": { + "description": "Webhook列表", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "post": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "创建新的 Webhook 配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "创建 Webhook", + "parameters": [ + { + "description": "Webhook信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.CreateWebhookRequest" + } + } + ], + "responses": { + "201": { + "description": "Webhook创建成功", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/handler.Response" + }, + { + "type": "object", + "properties": { + "data": { + "$ref": "#/definitions/domain.Webhook" + } + } + } + ] + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/webhooks/{id}": { + "put": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "更新指定 Webhook 的配置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "更新 Webhook", + "parameters": [ + { + "type": "integer", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "更新信息", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/service.UpdateWebhookRequest" + } + } + ], + "responses": { + "200": { + "description": "更新成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + }, + "delete": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "删除指定的 Webhook 配置", + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "删除 Webhook", + "parameters": [ + { + "type": "integer", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "删除成功", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + }, + "/api/v1/webhooks/{id}/deliveries": { + "get": { + "security": [ + { + "BearerAuth": [] + } + ], + "description": "获取指定 Webhook 的最近投递记录", + "produces": [ + "application/json" + ], + "tags": [ + "Webhook管理" + ], + "summary": "获取 Webhook 投递记录", + "parameters": [ + { + "type": "integer", + "description": "Webhook ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "default": 20, + "description": "返回记录数量", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "投递记录列表", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "400": { + "description": "请求参数错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "401": { + "description": "未认证", + "schema": { + "$ref": "#/definitions/handler.Response" + } + }, + "500": { + "description": "服务器错误", + "schema": { + "$ref": "#/definitions/handler.Response" + } + } + } + } + } + }, + "definitions": { + "auth.OAuthProvider": { + "type": "string", + "enum": [ + "wechat", + "qq", + "weibo", + "google", + "facebook", + "twitter", + "github", + "alipay", + "douyin" + ], + "x-enum-varnames": [ + "OAuthProviderWeChat", + "OAuthProviderQQ", + "OAuthProviderWeibo", + "OAuthProviderGoogle", + "OAuthProviderFacebook", + "OAuthProviderTwitter", + "OAuthProviderGitHub", + "OAuthProviderAlipay", + "OAuthProviderDouyin" + ] + }, + "auth.OAuthProviderInfo": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "provider": { + "$ref": "#/definitions/auth.OAuthProvider" + } + } + }, + "domain.UserStatus": { + "type": "integer", + "enum": [ + 0, + 1, + 2, + 3 + ], + "x-enum-comments": { + "UserStatusActive": "已激活", + "UserStatusDisabled": "已禁用", + "UserStatusInactive": "未激活", + "UserStatusLocked": "已锁定" + }, + "x-enum-descriptions": [ + "未激活", + "已激活", + "已锁定", + "已禁用" + ], + "x-enum-varnames": [ + "UserStatusInactive", + "UserStatusActive", + "UserStatusLocked", + "UserStatusDisabled" + ] + }, + "domain.Webhook": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "created_by": { + "type": "integer" + }, + "events": { + "description": "JSON 数组,订阅的事件类型", + "type": "string" + }, + "id": { + "type": "integer" + }, + "max_retries": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/domain.WebhookStatus" + }, + "timeout_sec": { + "type": "integer" + }, + "updated_at": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "domain.WebhookEventType": { + "type": "string", + "enum": [ + "user.registered", + "user.login", + "user.logout", + "user.updated", + "user.deleted", + "user.locked", + "user.password_changed", + "user.password_reset", + "user.totp_enabled", + "user.totp_disabled", + "user.login_failed", + "security.anomaly_detected" + ], + "x-enum-varnames": [ + "EventUserRegistered", + "EventUserLogin", + "EventUserLogout", + "EventUserUpdated", + "EventUserDeleted", + "EventUserLocked", + "EventPasswordChanged", + "EventPasswordReset", + "EventTOTPEnabled", + "EventTOTPDisabled", + "EventLoginFailed", + "EventAnomalyDetected" + ] + }, + "domain.WebhookStatus": { + "type": "integer", + "enum": [ + 1, + 0 + ], + "x-enum-varnames": [ + "WebhookStatusActive", + "WebhookStatusInactive" + ] + }, + "handler.ActivateEmailRequest": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "handler.AssignPermissionsRequest": { + "type": "object", + "properties": { + "permission_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "handler.AssignRolesRequest": { + "type": "object", + "properties": { + "role_ids": { + "type": "array", + "items": { + "type": "integer" + } + } + } + }, + "handler.AvatarResponse": { + "type": "object", + "properties": { + "avatar_url": { + "type": "string" + }, + "thumbnail": { + "type": "string" + } + } + }, + "handler.BootstrapAdminRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.CSRFTokenResponse": { + "type": "object", + "properties": { + "token": { + "type": "string" + } + } + }, + "handler.CaptchaResponse": { + "type": "object", + "properties": { + "captcha_id": { + "type": "string" + }, + "image": { + "type": "string" + } + } + }, + "handler.CreateAdminRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.CreateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.CustomFieldValuesResponse": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "handler.DeviceListResponse": { + "type": "object", + "properties": { + "cursor": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "items": {}, + "next_cursor": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.DisableTOTPRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "handler.EnableTOTPRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + } + } + }, + "handler.ForgotPasswordByPhoneRequest": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string" + } + } + }, + "handler.ForgotPasswordRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.IntrospectResponse": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "exp": { + "type": "integer" + }, + "scope": { + "type": "string" + }, + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "handler.LoginByEmailCodeRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "email": { + "type": "string" + } + } + }, + "handler.LoginLogListResponse": { + "type": "object", + "properties": { + "cursor": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "items": {}, + "list": {}, + "next_cursor": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.OAuthProvidersResponse": { + "type": "object", + "properties": { + "providers": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "handler.OperationLogListResponse": { + "type": "object", + "properties": { + "cursor": { + "type": "string" + }, + "has_more": { + "type": "boolean" + }, + "items": {}, + "list": {}, + "next_cursor": { + "type": "string" + }, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.RefreshTokenRequest": { + "type": "object", + "properties": { + "refresh_token": { + "type": "string" + } + } + }, + "handler.ResendActivationRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.ResetPasswordByPhoneRequest": { + "type": "object", + "required": [ + "code", + "new_password", + "phone" + ], + "properties": { + "code": { + "type": "string" + }, + "new_password": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "handler.ResetPasswordRequest": { + "type": "object", + "properties": { + "new_password": { + "type": "string" + }, + "token": { + "type": "string" + } + } + }, + "handler.Response": { + "type": "object", + "properties": { + "code": { + "type": "integer" + }, + "data": {}, + "message": { + "type": "string" + } + } + }, + "handler.RoleListResponse": { + "type": "object", + "properties": { + "items": {}, + "page": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "handler.SMSLoginRequest": { + "type": "object", + "required": [ + "code", + "phone" + ], + "properties": { + "code": { + "type": "string" + }, + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "phone": { + "type": "string" + } + } + }, + "handler.SendEmailCodeRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + } + } + }, + "handler.SetUserFieldValuesRequest": { + "type": "object", + "properties": { + "values": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + } + }, + "handler.SwaggerCustomField": { + "type": "object", + "properties": { + "active": { + "type": "boolean" + }, + "created_at": { + "type": "string" + }, + "field_key": { + "type": "string" + }, + "field_type": { + "type": "string" + }, + "help_text": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "options": { + "type": "string" + }, + "placeholder": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "sort_order": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.SwaggerDevice": { + "type": "object", + "properties": { + "created_at": { + "type": "string" + }, + "current": { + "type": "boolean" + }, + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "device_type": { + "type": "integer" + }, + "id": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "is_trusted": { + "type": "boolean" + }, + "last_active_at": { + "type": "string" + }, + "last_used_at": { + "type": "string" + }, + "location": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "trusted_until": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "handler.SwaggerPermission": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.SwaggerRole": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "description": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "is_system": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "updated_at": { + "type": "string" + } + } + }, + "handler.SwaggerTheme": { + "type": "object", + "properties": { + "accent_color": { + "type": "string" + }, + "background_color": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "error_color": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "info_color": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "string" + }, + "primary_color": { + "type": "string" + }, + "secondary_color": { + "type": "string" + }, + "success_color": { + "type": "string" + }, + "text_color": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "warning_color": { + "type": "string" + } + } + }, + "handler.TOTPSetupResponse": { + "type": "object", + "properties": { + "qr_code_base64": { + "type": "string" + }, + "recovery_codes": { + "type": "array", + "items": { + "type": "string" + } + }, + "secret": { + "type": "string" + } + } + }, + "handler.TOTPStatusResponse": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "handler.TOTPVerifyRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "temp_token": { + "type": "string" + }, + "user_id": { + "type": "integer" + } + } + }, + "handler.TokenResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "scope": { + "type": "string" + }, + "token_type": { + "type": "string" + } + } + }, + "handler.TrustDeviceRequest": { + "type": "object", + "properties": { + "trust_duration": { + "description": "信任持续时间,如 \"30d\" 表示30天", + "type": "string" + } + } + }, + "handler.UpdateDeviceStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdatePasswordRequest": { + "type": "object", + "properties": { + "new_password": { + "type": "string" + }, + "old_password": { + "type": "string" + } + } + }, + "handler.UpdatePermissionStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdateRoleStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdateStatusRequest": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + }, + "handler.UpdateUserRequest": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + } + } + }, + "handler.UserInfoResponse": { + "type": "object", + "properties": { + "user_id": { + "type": "integer" + }, + "username": { + "type": "string" + } + } + }, + "handler.UserListResponse": { + "type": "object", + "properties": { + "has_more": { + "type": "boolean" + }, + "items": {}, + "limit": { + "type": "integer" + }, + "next_cursor": { + "type": "string" + }, + "offset": { + "type": "integer" + }, + "page_size": { + "type": "integer" + }, + "total": { + "type": "integer" + }, + "users": {} + } + }, + "handler.UserResponse": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "status": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "handler.ValidateResetTokenRequest": { + "type": "object", + "required": [ + "token" + ], + "properties": { + "token": { + "type": "string" + } + } + }, + "handler.ValidateTokenResponse": { + "type": "object", + "properties": { + "valid": { + "type": "boolean" + } + } + }, + "handler.VerifyCaptchaRequest": { + "type": "object", + "properties": { + "answer": { + "type": "string" + }, + "captcha_id": { + "type": "string" + } + } + }, + "handler.VerifyResponse": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + } + } + }, + "handler.VerifyTOTPRequest": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "device_id": { + "type": "string" + } + } + }, + "handler.VerifyTOTPResponse": { + "type": "object", + "properties": { + "verified": { + "type": "boolean" + } + } + }, + "service.AuthCapabilities": { + "type": "object", + "properties": { + "admin_bootstrap_required": { + "type": "boolean" + }, + "email_activation": { + "type": "boolean" + }, + "email_code": { + "type": "boolean" + }, + "oauth_providers": { + "type": "array", + "items": { + "$ref": "#/definitions/auth.OAuthProviderInfo" + } + }, + "password": { + "type": "boolean" + }, + "password_reset": { + "type": "boolean" + }, + "sms_code": { + "type": "boolean" + } + } + }, + "service.BatchDeleteRequest": { + "type": "object", + "required": [ + "ids" + ], + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + } + } + }, + "service.BatchUpdateStatusRequest": { + "type": "object", + "required": [ + "ids", + "status" + ], + "properties": { + "ids": { + "type": "array", + "minItems": 1, + "items": { + "type": "integer" + } + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + } + } + }, + "service.CreateDeviceRequest": { + "type": "object", + "required": [ + "device_id" + ], + "properties": { + "device_browser": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "device_type": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "location": { + "type": "string" + } + } + }, + "service.CreateFieldRequest": { + "type": "object", + "required": [ + "field_key", + "name", + "type" + ], + "properties": { + "default": { + "type": "string" + }, + "field_key": { + "type": "string" + }, + "max_len": { + "type": "integer" + }, + "max_val": { + "type": "number" + }, + "min_len": { + "type": "integer" + }, + "min_val": { + "type": "number" + }, + "name": { + "type": "string" + }, + "options": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "sort": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.CreatePermissionRequest": { + "type": "object", + "required": [ + "code", + "name", + "type" + ], + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sort": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.CreateRoleRequest": { + "type": "object", + "required": [ + "code", + "name" + ], + "properties": { + "code": { + "type": "string" + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + } + } + }, + "service.CreateThemeRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "background_color": { + "type": "string" + }, + "custom_css": { + "type": "string" + }, + "custom_js": { + "type": "string" + }, + "favicon_url": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "logo_url": { + "type": "string" + }, + "name": { + "type": "string" + }, + "primary_color": { + "type": "string" + }, + "secondary_color": { + "type": "string" + }, + "text_color": { + "type": "string" + } + } + }, + "service.CreateWebhookRequest": { + "type": "object", + "required": [ + "events", + "name", + "url" + ], + "properties": { + "events": { + "type": "array", + "minItems": 1, + "items": { + "$ref": "#/definitions/domain.WebhookEventType" + } + }, + "name": { + "type": "string" + }, + "secret": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "service.DashboardStats": { + "type": "object", + "properties": { + "logins": { + "$ref": "#/definitions/service.LoginStats" + }, + "users": { + "$ref": "#/definitions/service.UserStats" + } + } + }, + "service.FeaturesInfo": { + "type": "object", + "properties": { + "data_export_enabled": { + "type": "boolean" + }, + "data_import_enabled": { + "type": "boolean" + }, + "email_verification": { + "type": "boolean" + }, + "login_log_enabled": { + "type": "boolean" + }, + "oauth_providers": { + "type": "array", + "items": { + "type": "string" + } + }, + "operation_log_enabled": { + "type": "boolean" + }, + "phone_verification": { + "type": "boolean" + }, + "sso_enabled": { + "type": "boolean" + } + } + }, + "service.LoginRequest": { + "type": "object", + "properties": { + "account": { + "type": "string" + }, + "device_browser": { + "description": "浏览器", + "type": "string" + }, + "device_id": { + "description": "设备唯一标识", + "type": "string" + }, + "device_name": { + "description": "设备名称", + "type": "string" + }, + "device_os": { + "description": "操作系统", + "type": "string" + }, + "email": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "remember": { + "description": "记住登录", + "type": "boolean" + }, + "username": { + "type": "string" + } + } + }, + "service.LoginResponse": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "expires_in": { + "type": "integer" + }, + "refresh_token": { + "type": "string" + }, + "requires_totp": { + "description": "RequiresTOTP 指示登录需要额外的TOTP验证(当设备未信任时)", + "type": "boolean" + }, + "temp_token": { + "description": "TempToken 临时令牌,用于TOTP验证阶段(短生命周期,不可用于常规API)", + "type": "string" + }, + "user": { + "$ref": "#/definitions/service.UserInfo" + }, + "user_id": { + "description": "UserID 当RequiresTOTP为true时返回,用于后续TOTP验证", + "type": "integer" + } + } + }, + "service.LoginStats": { + "type": "object", + "properties": { + "logins_today_failed": { + "type": "integer" + }, + "logins_today_success": { + "type": "integer" + }, + "logins_week": { + "type": "integer" + } + } + }, + "service.LogoutRequest": { + "type": "object", + "properties": { + "access_token": { + "type": "string" + }, + "refresh_token": { + "type": "string" + } + } + }, + "service.RegisterRequest": { + "type": "object", + "required": [ + "password", + "username" + ], + "properties": { + "email": { + "type": "string" + }, + "nickname": { + "type": "string" + }, + "password": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "phone_code": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "service.SecurityInfo": { + "type": "object", + "properties": { + "device_trust_duration": { + "description": "秒", + "type": "integer" + }, + "login_fail_duration": { + "description": "分钟", + "type": "integer" + }, + "login_fail_lock": { + "type": "boolean" + }, + "login_fail_threshold": { + "type": "integer" + }, + "password_history": { + "type": "integer" + }, + "password_min_length": { + "type": "integer" + }, + "password_require_lowercase": { + "type": "boolean" + }, + "password_require_numbers": { + "type": "boolean" + }, + "password_require_symbols": { + "type": "boolean" + }, + "password_require_uppercase": { + "type": "boolean" + }, + "session_timeout": { + "description": "秒", + "type": "integer" + }, + "totp_enabled": { + "type": "boolean" + } + } + }, + "service.SendCodeRequest": { + "type": "object", + "required": [ + "phone" + ], + "properties": { + "phone": { + "type": "string" + }, + "purpose": { + "type": "string" + }, + "scene": { + "type": "string" + } + } + }, + "service.SystemInfo": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "environment": { + "type": "string" + }, + "name": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "service.SystemSettings": { + "type": "object", + "properties": { + "features": { + "$ref": "#/definitions/service.FeaturesInfo" + }, + "security": { + "$ref": "#/definitions/service.SecurityInfo" + }, + "system": { + "$ref": "#/definitions/service.SystemInfo" + } + } + }, + "service.UpdateDeviceRequest": { + "type": "object", + "properties": { + "device_browser": { + "type": "string" + }, + "device_name": { + "type": "string" + }, + "device_os": { + "type": "string" + }, + "device_type": { + "type": "integer" + }, + "ip": { + "type": "string" + }, + "location": { + "type": "string" + }, + "status": { + "type": "integer" + } + } + }, + "service.UpdateFieldRequest": { + "type": "object", + "properties": { + "default": { + "type": "string" + }, + "max_len": { + "type": "integer" + }, + "max_val": { + "type": "number" + }, + "min_len": { + "type": "integer" + }, + "min_val": { + "type": "number" + }, + "name": { + "type": "string" + }, + "options": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "sort": { + "type": "integer" + }, + "status": { + "type": "integer" + }, + "type": { + "type": "integer" + } + } + }, + "service.UpdatePermissionRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + }, + "path": { + "type": "string" + }, + "sort": { + "type": "integer" + } + } + }, + "service.UpdateRoleRequest": { + "type": "object", + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parent_id": { + "type": "integer" + } + } + }, + "service.UpdateThemeRequest": { + "type": "object", + "properties": { + "background_color": { + "type": "string" + }, + "custom_css": { + "type": "string" + }, + "custom_js": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "favicon_url": { + "type": "string" + }, + "is_default": { + "type": "boolean" + }, + "logo_url": { + "type": "string" + }, + "primary_color": { + "type": "string" + }, + "secondary_color": { + "type": "string" + }, + "text_color": { + "type": "string" + } + } + }, + "service.UpdateWebhookRequest": { + "type": "object", + "properties": { + "events": { + "type": "array", + "items": { + "$ref": "#/definitions/domain.WebhookEventType" + } + }, + "name": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/domain.WebhookStatus" + }, + "url": { + "type": "string" + } + } + }, + "service.UserInfo": { + "type": "object", + "properties": { + "avatar": { + "type": "string" + }, + "email": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "nickname": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "status": { + "$ref": "#/definitions/domain.UserStatus" + }, + "username": { + "type": "string" + } + } + }, + "service.UserStats": { + "type": "object", + "properties": { + "active_users": { + "type": "integer" + }, + "disabled_users": { + "type": "integer" + }, + "inactive_users": { + "type": "integer" + }, + "locked_users": { + "type": "integer" + }, + "new_users_month": { + "type": "integer" + }, + "new_users_today": { + "type": "integer" + }, + "new_users_week": { + "type": "integer" + }, + "total_users": { + "type": "integer" + } + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..dc12ac5 --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,5012 @@ +basePath: /api/v1 +definitions: + auth.OAuthProvider: + enum: + - wechat + - qq + - weibo + - google + - facebook + - twitter + - github + - alipay + - douyin + type: string + x-enum-varnames: + - OAuthProviderWeChat + - OAuthProviderQQ + - OAuthProviderWeibo + - OAuthProviderGoogle + - OAuthProviderFacebook + - OAuthProviderTwitter + - OAuthProviderGitHub + - OAuthProviderAlipay + - OAuthProviderDouyin + auth.OAuthProviderInfo: + properties: + enabled: + type: boolean + name: + type: string + provider: + $ref: '#/definitions/auth.OAuthProvider' + type: object + domain.UserStatus: + enum: + - 0 + - 1 + - 2 + - 3 + type: integer + x-enum-comments: + UserStatusActive: 已激活 + UserStatusDisabled: 已禁用 + UserStatusInactive: 未激活 + UserStatusLocked: 已锁定 + x-enum-descriptions: + - 未激活 + - 已激活 + - 已锁定 + - 已禁用 + x-enum-varnames: + - UserStatusInactive + - UserStatusActive + - UserStatusLocked + - UserStatusDisabled + domain.Webhook: + properties: + created_at: + type: string + created_by: + type: integer + events: + description: JSON 数组,订阅的事件类型 + type: string + id: + type: integer + max_retries: + type: integer + name: + type: string + status: + $ref: '#/definitions/domain.WebhookStatus' + timeout_sec: + type: integer + updated_at: + type: string + url: + type: string + type: object + domain.WebhookEventType: + enum: + - user.registered + - user.login + - user.logout + - user.updated + - user.deleted + - user.locked + - user.password_changed + - user.password_reset + - user.totp_enabled + - user.totp_disabled + - user.login_failed + - security.anomaly_detected + type: string + x-enum-varnames: + - EventUserRegistered + - EventUserLogin + - EventUserLogout + - EventUserUpdated + - EventUserDeleted + - EventUserLocked + - EventPasswordChanged + - EventPasswordReset + - EventTOTPEnabled + - EventTOTPDisabled + - EventLoginFailed + - EventAnomalyDetected + domain.WebhookStatus: + enum: + - 1 + - 0 + type: integer + x-enum-varnames: + - WebhookStatusActive + - WebhookStatusInactive + handler.ActivateEmailRequest: + properties: + token: + type: string + required: + - token + type: object + handler.AssignPermissionsRequest: + properties: + permission_ids: + items: + type: integer + type: array + type: object + handler.AssignRolesRequest: + properties: + role_ids: + items: + type: integer + type: array + type: object + handler.AvatarResponse: + properties: + avatar_url: + type: string + thumbnail: + type: string + type: object + handler.BootstrapAdminRequest: + properties: + email: + type: string + password: + type: string + username: + type: string + type: object + handler.CSRFTokenResponse: + properties: + token: + type: string + type: object + handler.CaptchaResponse: + properties: + captcha_id: + type: string + image: + type: string + type: object + handler.CreateAdminRequest: + properties: + email: + type: string + nickname: + type: string + password: + type: string + username: + type: string + type: object + handler.CreateUserRequest: + properties: + email: + type: string + nickname: + type: string + password: + type: string + phone: + type: string + username: + type: string + type: object + handler.CustomFieldValuesResponse: + additionalProperties: + type: string + type: object + handler.DeviceListResponse: + properties: + cursor: + type: string + has_more: + type: boolean + items: {} + next_cursor: + type: string + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + handler.DisableTOTPRequest: + properties: + code: + type: string + type: object + handler.EnableTOTPRequest: + properties: + code: + type: string + type: object + handler.ForgotPasswordByPhoneRequest: + properties: + phone: + type: string + required: + - phone + type: object + handler.ForgotPasswordRequest: + properties: + email: + type: string + type: object + handler.IntrospectResponse: + properties: + active: + type: boolean + exp: + type: integer + scope: + type: string + user_id: + type: integer + username: + type: string + type: object + handler.LoginByEmailCodeRequest: + properties: + code: + type: string + device_browser: + type: string + device_id: + type: string + device_name: + type: string + device_os: + type: string + email: + type: string + type: object + handler.LoginLogListResponse: + properties: + cursor: + type: string + has_more: + type: boolean + items: {} + list: {} + next_cursor: + type: string + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + handler.OAuthProvidersResponse: + properties: + providers: + items: + type: string + type: array + type: object + handler.OperationLogListResponse: + properties: + cursor: + type: string + has_more: + type: boolean + items: {} + list: {} + next_cursor: + type: string + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + handler.RefreshTokenRequest: + properties: + refresh_token: + type: string + type: object + handler.ResendActivationRequest: + properties: + email: + type: string + type: object + handler.ResetPasswordByPhoneRequest: + properties: + code: + type: string + new_password: + type: string + phone: + type: string + required: + - code + - new_password + - phone + type: object + handler.ResetPasswordRequest: + properties: + new_password: + type: string + token: + type: string + type: object + handler.Response: + properties: + code: + type: integer + data: {} + message: + type: string + type: object + handler.RoleListResponse: + properties: + items: {} + page: + type: integer + page_size: + type: integer + total: + type: integer + type: object + handler.SMSLoginRequest: + properties: + code: + type: string + device_browser: + type: string + device_id: + type: string + device_name: + type: string + device_os: + type: string + phone: + type: string + required: + - code + - phone + type: object + handler.SendEmailCodeRequest: + properties: + email: + type: string + type: object + handler.SetUserFieldValuesRequest: + properties: + values: + additionalProperties: + type: string + type: object + type: object + handler.SwaggerCustomField: + properties: + active: + type: boolean + created_at: + type: string + field_key: + type: string + field_type: + type: string + help_text: + type: string + id: + type: integer + name: + type: string + options: + type: string + placeholder: + type: string + required: + type: boolean + sort_order: + type: integer + updated_at: + type: string + type: object + handler.SwaggerDevice: + properties: + created_at: + type: string + current: + type: boolean + device_browser: + type: string + device_id: + type: string + device_name: + type: string + device_os: + type: string + device_type: + type: integer + id: + type: integer + ip: + type: string + is_trusted: + type: boolean + last_active_at: + type: string + last_used_at: + type: string + location: + type: string + status: + type: integer + trusted_until: + type: string + updated_at: + type: string + user_id: + type: integer + type: object + handler.SwaggerPermission: + properties: + code: + type: string + created_at: + type: string + description: + type: string + icon: + type: string + id: + type: integer + method: + type: string + name: + type: string + parent_id: + type: integer + path: + type: string + sort: + type: integer + status: + type: integer + type: + type: integer + updated_at: + type: string + type: object + handler.SwaggerRole: + properties: + code: + type: string + created_at: + type: string + description: + type: string + id: + type: integer + is_system: + type: boolean + name: + type: string + sort: + type: integer + status: + type: integer + updated_at: + type: string + type: object + handler.SwaggerTheme: + properties: + accent_color: + type: string + background_color: + type: string + created_at: + type: string + error_color: + type: string + id: + type: integer + info_color: + type: string + is_default: + type: boolean + name: + type: string + primary_color: + type: string + secondary_color: + type: string + success_color: + type: string + text_color: + type: string + updated_at: + type: string + warning_color: + type: string + type: object + handler.TOTPSetupResponse: + properties: + qr_code_base64: + type: string + recovery_codes: + items: + type: string + type: array + secret: + type: string + type: object + handler.TOTPStatusResponse: + properties: + enabled: + type: boolean + type: object + handler.TOTPVerifyRequest: + properties: + code: + type: string + device_id: + type: string + temp_token: + type: string + user_id: + type: integer + type: object + handler.TokenResponse: + properties: + access_token: + type: string + expires_in: + type: integer + scope: + type: string + token_type: + type: string + type: object + handler.TrustDeviceRequest: + properties: + trust_duration: + description: 信任持续时间,如 "30d" 表示30天 + type: string + type: object + handler.UpdateDeviceStatusRequest: + properties: + status: + type: string + type: object + handler.UpdatePasswordRequest: + properties: + new_password: + type: string + old_password: + type: string + type: object + handler.UpdatePermissionStatusRequest: + properties: + status: + type: string + type: object + handler.UpdateRoleStatusRequest: + properties: + status: + type: string + type: object + handler.UpdateStatusRequest: + properties: + status: + type: string + type: object + handler.UpdateUserRequest: + properties: + email: + type: string + nickname: + type: string + type: object + handler.UserInfoResponse: + properties: + user_id: + type: integer + username: + type: string + type: object + handler.UserListResponse: + properties: + has_more: + type: boolean + items: {} + limit: + type: integer + next_cursor: + type: string + offset: + type: integer + page_size: + type: integer + total: + type: integer + users: {} + type: object + handler.UserResponse: + properties: + email: + type: string + id: + type: integer + nickname: + type: string + status: + type: string + username: + type: string + type: object + handler.ValidateResetTokenRequest: + properties: + token: + type: string + required: + - token + type: object + handler.ValidateTokenResponse: + properties: + valid: + type: boolean + type: object + handler.VerifyCaptchaRequest: + properties: + answer: + type: string + captcha_id: + type: string + type: object + handler.VerifyResponse: + properties: + verified: + type: boolean + type: object + handler.VerifyTOTPRequest: + properties: + code: + type: string + device_id: + type: string + type: object + handler.VerifyTOTPResponse: + properties: + verified: + type: boolean + type: object + service.AuthCapabilities: + properties: + admin_bootstrap_required: + type: boolean + email_activation: + type: boolean + email_code: + type: boolean + oauth_providers: + items: + $ref: '#/definitions/auth.OAuthProviderInfo' + type: array + password: + type: boolean + password_reset: + type: boolean + sms_code: + type: boolean + type: object + service.BatchDeleteRequest: + properties: + ids: + items: + type: integer + minItems: 1 + type: array + required: + - ids + type: object + service.BatchUpdateStatusRequest: + properties: + ids: + items: + type: integer + minItems: 1 + type: array + status: + $ref: '#/definitions/domain.UserStatus' + required: + - ids + - status + type: object + service.CreateDeviceRequest: + properties: + device_browser: + type: string + device_id: + type: string + device_name: + type: string + device_os: + type: string + device_type: + type: integer + ip: + type: string + location: + type: string + required: + - device_id + type: object + service.CreateFieldRequest: + properties: + default: + type: string + field_key: + type: string + max_len: + type: integer + max_val: + type: number + min_len: + type: integer + min_val: + type: number + name: + type: string + options: + type: string + required: + type: boolean + sort: + type: integer + type: + type: integer + required: + - field_key + - name + - type + type: object + service.CreatePermissionRequest: + properties: + code: + type: string + description: + type: string + icon: + type: string + method: + type: string + name: + type: string + parent_id: + type: integer + path: + type: string + sort: + type: integer + type: + type: integer + required: + - code + - name + - type + type: object + service.CreateRoleRequest: + properties: + code: + type: string + description: + type: string + name: + type: string + parent_id: + type: integer + required: + - code + - name + type: object + service.CreateThemeRequest: + properties: + background_color: + type: string + custom_css: + type: string + custom_js: + type: string + favicon_url: + type: string + is_default: + type: boolean + logo_url: + type: string + name: + type: string + primary_color: + type: string + secondary_color: + type: string + text_color: + type: string + required: + - name + type: object + service.CreateWebhookRequest: + properties: + events: + items: + $ref: '#/definitions/domain.WebhookEventType' + minItems: 1 + type: array + name: + type: string + secret: + type: string + url: + type: string + required: + - events + - name + - url + type: object + service.DashboardStats: + properties: + logins: + $ref: '#/definitions/service.LoginStats' + users: + $ref: '#/definitions/service.UserStats' + type: object + service.FeaturesInfo: + properties: + data_export_enabled: + type: boolean + data_import_enabled: + type: boolean + email_verification: + type: boolean + login_log_enabled: + type: boolean + oauth_providers: + items: + type: string + type: array + operation_log_enabled: + type: boolean + phone_verification: + type: boolean + sso_enabled: + type: boolean + type: object + service.LoginRequest: + properties: + account: + type: string + device_browser: + description: 浏览器 + type: string + device_id: + description: 设备唯一标识 + type: string + device_name: + description: 设备名称 + type: string + device_os: + description: 操作系统 + type: string + email: + type: string + password: + type: string + phone: + type: string + remember: + description: 记住登录 + type: boolean + username: + type: string + type: object + service.LoginResponse: + properties: + access_token: + type: string + expires_in: + type: integer + refresh_token: + type: string + requires_totp: + description: RequiresTOTP 指示登录需要额外的TOTP验证(当设备未信任时) + type: boolean + temp_token: + description: TempToken 临时令牌,用于TOTP验证阶段(短生命周期,不可用于常规API) + type: string + user: + $ref: '#/definitions/service.UserInfo' + user_id: + description: UserID 当RequiresTOTP为true时返回,用于后续TOTP验证 + type: integer + type: object + service.LoginStats: + properties: + logins_today_failed: + type: integer + logins_today_success: + type: integer + logins_week: + type: integer + type: object + service.LogoutRequest: + properties: + access_token: + type: string + refresh_token: + type: string + type: object + service.RegisterRequest: + properties: + email: + type: string + nickname: + type: string + password: + type: string + phone: + type: string + phone_code: + type: string + username: + type: string + required: + - password + - username + type: object + service.SecurityInfo: + properties: + device_trust_duration: + description: 秒 + type: integer + login_fail_duration: + description: 分钟 + type: integer + login_fail_lock: + type: boolean + login_fail_threshold: + type: integer + password_history: + type: integer + password_min_length: + type: integer + password_require_lowercase: + type: boolean + password_require_numbers: + type: boolean + password_require_symbols: + type: boolean + password_require_uppercase: + type: boolean + session_timeout: + description: 秒 + type: integer + totp_enabled: + type: boolean + type: object + service.SendCodeRequest: + properties: + phone: + type: string + purpose: + type: string + scene: + type: string + required: + - phone + type: object + service.SystemInfo: + properties: + description: + type: string + environment: + type: string + name: + type: string + version: + type: string + type: object + service.SystemSettings: + properties: + features: + $ref: '#/definitions/service.FeaturesInfo' + security: + $ref: '#/definitions/service.SecurityInfo' + system: + $ref: '#/definitions/service.SystemInfo' + type: object + service.UpdateDeviceRequest: + properties: + device_browser: + type: string + device_name: + type: string + device_os: + type: string + device_type: + type: integer + ip: + type: string + location: + type: string + status: + type: integer + type: object + service.UpdateFieldRequest: + properties: + default: + type: string + max_len: + type: integer + max_val: + type: number + min_len: + type: integer + min_val: + type: number + name: + type: string + options: + type: string + required: + type: boolean + sort: + type: integer + status: + type: integer + type: + type: integer + type: object + service.UpdatePermissionRequest: + properties: + description: + type: string + icon: + type: string + method: + type: string + name: + type: string + parent_id: + type: integer + path: + type: string + sort: + type: integer + type: object + service.UpdateRoleRequest: + properties: + description: + type: string + name: + type: string + parent_id: + type: integer + type: object + service.UpdateThemeRequest: + properties: + background_color: + type: string + custom_css: + type: string + custom_js: + type: string + enabled: + type: boolean + favicon_url: + type: string + is_default: + type: boolean + logo_url: + type: string + primary_color: + type: string + secondary_color: + type: string + text_color: + type: string + type: object + service.UpdateWebhookRequest: + properties: + events: + items: + $ref: '#/definitions/domain.WebhookEventType' + type: array + name: + type: string + status: + $ref: '#/definitions/domain.WebhookStatus' + url: + type: string + type: object + service.UserInfo: + properties: + avatar: + type: string + email: + type: string + id: + type: integer + nickname: + type: string + phone: + type: string + status: + $ref: '#/definitions/domain.UserStatus' + username: + type: string + type: object + service.UserStats: + properties: + active_users: + type: integer + disabled_users: + type: integer + inactive_users: + type: integer + locked_users: + type: integer + new_users_month: + type: integer + new_users_today: + type: integer + new_users_week: + type: integer + total_users: + type: integer + type: object +info: + contact: {} + description: API for user management, authentication, authorization, and administration. + title: User Management System API + version: "1.0" +paths: + /api/v1/admin/admins: + get: + description: 获取所有管理员用户列表(仅管理员) + produces: + - application/json + responses: + "200": + description: 管理员列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.UserResponse' + type: array + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取管理员列表 + tags: + - 用户管理 + post: + consumes: + - application/json + description: 创建新管理员账号(仅管理员) + parameters: + - description: 管理员信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.CreateAdminRequest' + produces: + - application/json + responses: + "201": + description: 管理员创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.UserResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建管理员 + tags: + - 用户管理 + /api/v1/admin/admins/{id}: + delete: + description: 删除管理员角色(最后管理员保护、自删保护)(仅管理员) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 管理员已移除 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 无效的用户ID + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "409": + description: 无法删除(最后管理员或自删) + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除管理员 + tags: + - 用户管理 + /api/v1/admin/devices: + get: + description: 获取所有设备列表(仅管理员),支持游标分页和偏移分页 + parameters: + - description: 游标分页游标 + in: query + name: cursor + type: string + - description: 每页数量(游标模式) + in: query + name: size + type: integer + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 设备列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.DeviceListResponse' + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取所有设备列表 + tags: + - 设备管理 + /api/v1/admin/settings: + get: + description: 获取系统配置、安全设置和功能开关信息 + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.SystemSettings' + type: object + security: + - BearerAuth: [] + summary: 获取系统设置 + tags: + - 系统设置 + /api/v1/admin/stats/dashboard: + get: + description: 获取系统仪表盘统计数据(仅管理员) + produces: + - application/json + responses: + "200": + description: 仪表盘数据 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.DashboardStats' + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取仪表盘统计 + tags: + - 统计 + /api/v1/admin/stats/users: + get: + description: 获取用户统计数据(仅管理员) + produces: + - application/json + responses: + "200": + description: 用户统计数据 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.UserStats' + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取用户统计 + tags: + - 统计 + /api/v1/admin/users/export: + get: + consumes: + - application/json + description: 导出用户数据为 CSV 或 Excel 格式 + parameters: + - default: csv + description: 导出格式 + enum: + - csv + - excel + in: query + name: format + type: string + - description: 导出字段,逗号分隔 + in: query + name: fields + type: string + - description: 关键词过滤 + in: query + name: keyword + type: string + - description: 用户状态过滤 + in: query + name: status + type: integer + produces: + - application/json + responses: + "200": + description: 用户数据文件 + schema: + type: file + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 导出用户数据 + tags: + - 数据导入导出 + /api/v1/admin/users/import: + post: + consumes: + - multipart/form-data + description: 从 CSV 或 Excel 文件导入用户数据 + parameters: + - description: 导入文件 + in: formData + name: file + required: true + type: file + - default: csv + description: 文件格式 + enum: + - csv + - excel + in: query + name: format + type: string + produces: + - application/json + responses: + "200": + description: 导入结果 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 导入用户数据 + tags: + - 数据导入导出 + /api/v1/admin/users/import/template: + get: + description: 下载用户批量导入的 CSV 或 Excel 模板 + parameters: + - default: csv + description: 模板格式 + enum: + - csv + - excel + in: query + name: format + type: string + produces: + - application/json + responses: + "200": + description: 导入模板文件 + schema: + type: file + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取用户导入模板 + tags: + - 数据导入导出 + /api/v1/auth/2fa/disable: + post: + consumes: + - application/json + description: 输入验证码禁用 TOTP 两步验证 + parameters: + - description: 验证码 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.DisableTOTPRequest' + produces: + - application/json + responses: + "200": + description: 禁用成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证或验证码错误 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 禁用 TOTP 两步验证 + tags: + - 两步验证 + /api/v1/auth/2fa/enable: + post: + consumes: + - application/json + description: 输入验证码启用 TOTP 两步验证 + parameters: + - description: 验证码 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.EnableTOTPRequest' + produces: + - application/json + responses: + "200": + description: 启用成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证或验证码错误 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 启用 TOTP 两步验证 + tags: + - 两步验证 + /api/v1/auth/2fa/setup: + get: + consumes: + - application/json + description: 为当前用户设置 TOTP 两步验证,返回密钥和二维码 + produces: + - application/json + responses: + "200": + description: TOTP设置信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.TOTPSetupResponse' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 设置 TOTP 两步验证 + tags: + - 两步验证 + /api/v1/auth/2fa/status: + get: + description: 获取当前用户的TOTP两步验证状态 + produces: + - application/json + responses: + "200": + description: TOTP状态 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.TOTPStatusResponse' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取TOTP状态 + tags: + - 两步验证 + /api/v1/auth/2fa/verify: + post: + consumes: + - application/json + description: 在登录或其他敏感操作时验证 TOTP 验证码 + parameters: + - description: 验证码 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.VerifyTOTPRequest' + produces: + - application/json + responses: + "200": + description: 验证结果 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.VerifyTOTPResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证或验证码错误 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 验证 TOTP 验证码 + tags: + - 两步验证 + /api/v1/auth/activate-email: + post: + consumes: + - application/json + description: 使用邮箱激活token激活用户账号 + parameters: + - description: 激活请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ActivateEmailRequest' + produces: + - application/json + responses: + "200": + description: 激活成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: token缺失 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: token无效或已过期 + schema: + $ref: '#/definitions/handler.Response' + summary: 激活用户邮箱 + tags: + - 邮箱认证 + /api/v1/auth/bootstrap-admin: + post: + consumes: + - application/json + description: 在系统未配置管理员时,创建第一个管理员账号(需要BOOTSTRAP_SECRET) + parameters: + - description: 引导密钥 + in: header + name: X-Bootstrap-Secret + required: true + type: string + - description: 管理员信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.BootstrapAdminRequest' + produces: + - application/json + responses: + "201": + description: 管理员创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.UserInfo' + type: object + "401": + description: 引导密钥无效 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 引导初始化未授权 + schema: + $ref: '#/definitions/handler.Response' + security: + - BootstrapSecret: [] + summary: 引导初始化管理员账号 + tags: + - 系统初始化 + /api/v1/auth/capabilities: + get: + description: 返回系统支持的认证方式和配置(如是否需要邮件激活、是否支持OAuth等) + produces: + - application/json + responses: + "200": + description: 认证能力配置 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.AuthCapabilities' + type: object + summary: 获取系统认证能力 + tags: + - 认证 + /api/v1/auth/captcha: + get: + description: 生成图形验证码 + produces: + - application/json + responses: + "200": + description: 验证码信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.CaptchaResponse' + type: object + summary: 生成验证码 + tags: + - 验证码 + /api/v1/auth/captcha/image: + get: + description: 根据captcha_id获取验证码图片(当前未实现) + parameters: + - description: 验证码ID + in: query + name: captcha_id + type: string + produces: + - application/json + responses: + "200": + description: 验证码图片 + schema: + $ref: '#/definitions/handler.Response' + summary: 获取验证码图片 + tags: + - 验证码 + /api/v1/auth/captcha/verify: + post: + consumes: + - application/json + description: 验证用户输入的验证码是否正确 + parameters: + - description: 验证码信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.VerifyCaptchaRequest' + produces: + - application/json + responses: + "200": + description: 验证成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.VerifyResponse' + type: object + "400": + description: 验证码无效 + schema: + $ref: '#/definitions/handler.Response' + summary: 验证验证码 + tags: + - 验证码 + /api/v1/auth/csrf-token: + get: + description: 由于系统使用JWT Bearer Token认证,不存在CSRF风险,返回空token + produces: + - application/json + responses: + "200": + description: CSRF token(为空) + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.CSRFTokenResponse' + type: object + summary: 获取CSRF令牌 + tags: + - 认证 + /api/v1/auth/forgot-password: + post: + consumes: + - application/json + description: 请求密码重置邮件 + parameters: + - description: 邮箱地址 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ForgotPasswordRequest' + produces: + - application/json + responses: + "200": + description: 密码重置邮件已发送 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + summary: 忘记密码 + tags: + - 密码重置 + /api/v1/auth/forgot-password/phone: + post: + consumes: + - application/json + description: 向绑定的手机号发送短信验证码用于重置密码 + parameters: + - description: 手机号 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ForgotPasswordByPhoneRequest' + produces: + - application/json + responses: + "200": + description: 验证码发送成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "503": + description: 短信服务未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 发送短信验证码(忘记密码) + tags: + - 密码重置 + /api/v1/auth/login: + post: + consumes: + - application/json + description: 用户使用账号密码登录,支持多种认证方式(用户名/邮箱/手机号) + parameters: + - description: 登录请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.LoginRequest' + produces: + - application/json + responses: + "200": + description: 登录成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.LoginResponse' + type: object + "400": + description: 请求参数错误 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + "401": + description: 认证失败 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + "429": + description: 登录尝试过多 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + summary: 用户登录 + tags: + - 认证 + /api/v1/auth/login/code: + post: + consumes: + - application/json + description: 使用手机号和短信验证码登录(带设备信息以支持设备信任链路) + parameters: + - description: 登录请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.SMSLoginRequest' + produces: + - application/json + responses: + "200": + description: 登录成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 验证码错误 + schema: + $ref: '#/definitions/handler.Response' + "503": + description: 短信登录未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 短信验证码登录 + tags: + - 短信验证 + /api/v1/auth/login/email-code: + post: + consumes: + - application/json + description: 使用邮箱和验证码完成登录 + parameters: + - description: 登录请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.LoginByEmailCodeRequest' + produces: + - application/json + responses: + "200": + description: 登录成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.LoginResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 验证码错误或已过期 + schema: + $ref: '#/definitions/handler.Response' + summary: 邮箱验证码登录 + tags: + - 邮箱认证 + /api/v1/auth/login/totp-verify: + post: + consumes: + - application/json + description: 当登录返回requires_totp=true时,使用此接口完成TOTP验证 + parameters: + - description: TOTP验证请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.TOTPVerifyRequest' + produces: + - application/json + responses: + "200": + description: 验证成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.LoginResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: TOTP验证失败 + schema: + $ref: '#/definitions/handler.Response' + summary: TOTP验证(密码登录后) + tags: + - 认证 + /api/v1/auth/logout: + post: + consumes: + - application/json + description: 使当前 access_token 和 refresh_token 失效 + parameters: + - description: 登出请求(token可从header获取) + in: body + name: request + schema: + $ref: '#/definitions/service.LogoutRequest' + produces: + - application/json + responses: + "200": + description: 登出成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + security: + - BearerAuth: [] + summary: 用户登出 + tags: + - 认证 + /api/v1/auth/oauth/{provider}: + get: + description: 发起OAuth登录流程(当前未配置) + parameters: + - description: OAuth提供商(如 github, google) + in: path + name: provider + required: true + type: string + produces: + - application/json + responses: + "200": + description: OAuth未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: OAuth登录初始化 + tags: + - OAuth + /api/v1/auth/oauth/{provider}/callback: + get: + description: 处理OAuth provider回调(当前未配置) + parameters: + - description: OAuth提供商 + in: path + name: provider + required: true + type: string + produces: + - application/json + responses: + "200": + description: OAuth未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: OAuth回调处理 + tags: + - OAuth + /api/v1/auth/oauth/exchange: + post: + consumes: + - application/json + description: 使用OAuth code交换access_token(当前未配置) + parameters: + - description: OAuth提供商 + in: path + name: provider + required: true + type: string + produces: + - application/json + responses: + "200": + description: OAuth未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: OAuth令牌交换 + tags: + - OAuth + /api/v1/auth/oauth/providers: + get: + description: 返回系统已配置并启用的OAuth提供商列表 + produces: + - application/json + responses: + "200": + description: 提供商列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.OAuthProvidersResponse' + type: object + summary: 获取OAuth提供商列表 + tags: + - OAuth + /api/v1/auth/password/validate: + post: + consumes: + - application/json + description: 验证密码重置链接中的 Token 是否有效 + parameters: + - description: 重置 Token + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ValidateResetTokenRequest' + produces: + - application/json + responses: + "200": + description: Token验证结果 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.ValidateTokenResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + summary: 验证密码重置 Token + tags: + - 密码重置 + /api/v1/auth/refresh: + post: + consumes: + - application/json + description: 使用 refresh_token 获取新的 access_token + parameters: + - description: 刷新令牌请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.RefreshTokenRequest' + produces: + - application/json + responses: + "200": + description: 刷新成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.LoginResponse' + type: object + "400": + description: 请求参数错误 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + "401": + description: refresh_token无效或已过期 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + summary: 刷新访问令牌 + tags: + - 认证 + /api/v1/auth/register: + post: + consumes: + - application/json + description: 用户注册新账号,支持用户名+密码或手机号注册 + parameters: + - description: 注册请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.RegisterRequest' + produces: + - application/json + responses: + "201": + description: 注册成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.UserInfo' + type: object + "400": + description: 请求参数错误 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + "409": + description: 用户已存在 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + summary: 用户注册 + tags: + - 认证 + /api/v1/auth/resend-activation: + post: + consumes: + - application/json + description: 重新发送账号激活邮件(防枚举:无论邮箱是否注册都返回成功) + parameters: + - description: 邮箱地址 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ResendActivationRequest' + produces: + - application/json + responses: + "200": + description: 激活邮件已发送(如果邮箱已注册) + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 邮箱格式错误 + schema: + $ref: '#/definitions/handler.Response' + summary: 重发激活邮件 + tags: + - 邮箱认证 + /api/v1/auth/reset-password: + post: + consumes: + - application/json + description: 使用 Token 重置密码 + parameters: + - description: 重置请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ResetPasswordRequest' + produces: + - application/json + responses: + "200": + description: 密码重置成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + summary: 重置密码 + tags: + - 密码重置 + /api/v1/auth/reset-password/phone: + post: + consumes: + - application/json + description: 使用短信验证码重置登录密码 + parameters: + - description: 重置请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.ResetPasswordByPhoneRequest' + produces: + - application/json + responses: + "200": + description: 密码重置成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 验证码错误 + schema: + $ref: '#/definitions/handler.Response' + "503": + description: 短信服务未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 通过短信验证码重置密码 + tags: + - 密码重置 + /api/v1/auth/send-code: + post: + consumes: + - application/json + description: 向指定手机号发送短信验证码(用于注册或登录) + parameters: + - description: 发送验证码请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.SendCodeRequest' + produces: + - application/json + responses: + "200": + description: 发送成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "503": + description: 短信服务未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 发送短信验证码 + tags: + - 短信验证 + /api/v1/auth/send-email-code: + post: + consumes: + - application/json + description: 发送邮箱登录验证码(防枚举:无论邮箱是否注册都返回成功) + parameters: + - description: 邮箱地址 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.SendEmailCodeRequest' + produces: + - application/json + responses: + "200": + description: 验证码已发送 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 邮箱格式错误 + schema: + $ref: '#/definitions/handler.Response' + summary: 发送邮箱验证码 + tags: + - 邮箱认证 + /api/v1/auth/userinfo: + get: + description: 获取已登录用户的详细信息 + produces: + - application/json + responses: + "200": + description: 用户信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/service.UserInfo' + type: object + "401": + description: 未认证 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + code: + type: integer + message: + type: string + type: object + security: + - BearerAuth: [] + summary: 获取当前用户信息 + tags: + - 认证 + /api/v1/custom-fields: + get: + description: 获取所有自定义字段定义列表 + produces: + - application/json + responses: + "200": + description: 字段列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.SwaggerCustomField' + type: array + type: object + security: + - BearerAuth: [] + summary: 获取自定义字段列表 + tags: + - 自定义字段 + post: + consumes: + - application/json + description: 创建新的自定义字段定义(仅管理员) + parameters: + - description: 字段定义 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.CreateFieldRequest' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerCustomField' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建自定义字段 + tags: + - 自定义字段 + /api/v1/custom-fields/{id}: + delete: + description: 删除自定义字段定义(仅管理员) + parameters: + - description: 字段ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 字段不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除自定义字段 + tags: + - 自定义字段 + get: + description: 根据ID获取自定义字段定义 + parameters: + - description: 字段ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 字段信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerCustomField' + type: object + "404": + description: 字段不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取自定义字段详情 + tags: + - 自定义字段 + put: + consumes: + - application/json + description: 更新自定义字段定义(仅管理员) + parameters: + - description: 字段ID + in: path + name: id + required: true + type: integer + - description: 更新信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.UpdateFieldRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerCustomField' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 字段不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新自定义字段 + tags: + - 自定义字段 + /api/v1/devices: + get: + description: 获取当前用户的所有设备记录 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 设备列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.DeviceListResponse' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取当前用户的设备列表 + tags: + - 设备管理 + post: + consumes: + - application/json + description: 当前用户创建设备记录 + parameters: + - description: 设备信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.CreateDeviceRequest' + produces: + - application/json + responses: + "201": + description: 设备创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerDevice' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建设备记录 + tags: + - 设备管理 + /api/v1/devices/{id}: + delete: + description: 删除设备记录 + parameters: + - description: 设备ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 设备不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除设备 + tags: + - 设备管理 + get: + description: 根据ID获取设备详细信息 + parameters: + - description: 设备ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 设备信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerDevice' + type: object + "404": + description: 设备不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取设备详情 + tags: + - 设备管理 + put: + consumes: + - application/json + description: 更新设备的基本信息 + parameters: + - description: 设备ID + in: path + name: id + required: true + type: integer + - description: 更新信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.UpdateDeviceRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerDevice' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 设备不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新设备信息 + tags: + - 设备管理 + /api/v1/devices/{id}/status: + put: + consumes: + - application/json + description: 更新设备状态(active/inactive) + parameters: + - description: 设备ID + in: path + name: id + required: true + type: integer + - description: 状态信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdateDeviceStatusRequest' + produces: + - application/json + responses: + "200": + description: 状态更新成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 无效的状态值 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 设备不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新设备状态 + tags: + - 设备管理 + /api/v1/devices/{id}/trust: + delete: + description: 取消设备的信任状态 + parameters: + - description: 设备ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 取消成功 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 设备不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 取消设备信任 + tags: + - 设备管理 + post: + consumes: + - application/json + description: 将指定设备设置为信任设备,在信任期内免二次验证 + parameters: + - description: 设备ID + in: path + name: id + required: true + type: integer + - description: 信任配置 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.TrustDeviceRequest' + produces: + - application/json + responses: + "200": + description: 设置成功 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 设备不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 设置设备为信任设备 + tags: + - 设备管理 + /api/v1/devices/by-device-id/{deviceId}/trust: + post: + consumes: + - application/json + description: 根据设备唯一标识字符串设置设备为信任状态 + parameters: + - description: 设备唯一标识 + in: path + name: deviceId + required: true + type: string + - description: 信任配置 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.TrustDeviceRequest' + produces: + - application/json + responses: + "200": + description: 设置成功 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 根据设备标识设置信任 + tags: + - 设备管理 + /api/v1/devices/me/logout-others: + post: + description: 登出当前用户除指定设备外的所有其他设备 + parameters: + - description: 当前设备ID + in: header + name: X-Device-ID + required: true + type: string + produces: + - application/json + responses: + "200": + description: 登出成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 无效的设备ID + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 登出其他设备 + tags: + - 设备管理 + /api/v1/devices/me/trusted: + get: + description: 获取当前用户的信任设备列表 + produces: + - application/json + responses: + "200": + description: 信任设备列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.SwaggerDevice' + type: array + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取信任设备列表 + tags: + - 设备管理 + /api/v1/devices/users/{id}: + get: + description: 获取指定用户的设备列表(仅本人或管理员) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 设备列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.DeviceListResponse' + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取用户设备列表 + tags: + - 设备管理 + /api/v1/logs/login: + get: + description: 获取所有登录日志(仅管理员),支持游标分页和偏移分页 + parameters: + - description: 游标分页游标 + in: query + name: cursor + type: string + - description: 每页数量(游标模式) + in: query + name: size + type: integer + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 登录日志列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.LoginLogListResponse' + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取登录日志列表 + tags: + - 日志 + /api/v1/logs/login/export: + get: + description: 导出登录日志为 CSV 文件 + parameters: + - description: 开始时间 + in: query + name: start_time + type: string + - description: 结束时间 + in: query + name: end_time + type: string + - description: 用户ID + format: int64 + in: query + name: user_id + type: integer + produces: + - application/json + responses: + "200": + description: CSV文件 + schema: + type: file + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 导出登录日志 + tags: + - 日志 + /api/v1/logs/login/me: + get: + description: 获取当前用户的登录日志 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 登录日志列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.LoginLogListResponse' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取登录日志 + tags: + - 日志 + /api/v1/logs/operation: + get: + description: 获取所有操作日志(仅管理员),支持游标分页和偏移分页 + parameters: + - description: 游标分页游标 + in: query + name: cursor + type: string + - description: 每页数量(游标模式) + in: query + name: size + type: integer + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 操作日志列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.OperationLogListResponse' + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取操作日志列表 + tags: + - 日志 + /api/v1/logs/operation/me: + get: + description: 获取当前用户的操作日志 + parameters: + - description: 页码 + in: query + name: page + type: integer + - description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: 操作日志列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.OperationLogListResponse' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取操作日志 + tags: + - 日志 + /api/v1/permissions: + get: + description: 获取系统权限列表 + produces: + - application/json + responses: + "200": + description: 权限列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.SwaggerPermission' + type: array + type: object + security: + - BearerAuth: [] + summary: 获取权限列表 + tags: + - 权限管理 + post: + consumes: + - application/json + description: 创建新的权限定义(仅管理员) + parameters: + - description: 权限信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.CreatePermissionRequest' + produces: + - application/json + responses: + "201": + description: 创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerPermission' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建权限 + tags: + - 权限管理 + /api/v1/permissions/{id}: + delete: + description: 删除权限定义(仅管理员) + parameters: + - description: 权限ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 权限不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除权限 + tags: + - 权限管理 + get: + description: 根据ID获取权限详细信息 + parameters: + - description: 权限ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 权限信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerPermission' + type: object + "404": + description: 权限不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取权限详情 + tags: + - 权限管理 + put: + consumes: + - application/json + description: 更新权限信息(仅管理员) + parameters: + - description: 权限ID + in: path + name: id + required: true + type: integer + - description: 更新信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.UpdatePermissionRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerPermission' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 权限不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新权限 + tags: + - 权限管理 + /api/v1/permissions/{id}/status: + put: + consumes: + - application/json + description: 更新权限状态(enabled/disabled)(仅管理员) + parameters: + - description: 权限ID + in: path + name: id + required: true + type: integer + - description: 状态信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdatePermissionStatusRequest' + produces: + - application/json + responses: + "200": + description: 状态更新成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 无效的状态值 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 权限不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新权限状态 + tags: + - 权限管理 + /api/v1/permissions/tree: + get: + description: 获取系统权限的树形结构 + produces: + - application/json + responses: + "200": + description: 权限树 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.SwaggerPermission' + type: array + type: object + security: + - BearerAuth: [] + summary: 获取权限树 + tags: + - 权限管理 + /api/v1/roles: + get: + description: 获取系统角色列表 + produces: + - application/json + responses: + "200": + description: 角色列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.RoleListResponse' + type: object + security: + - BearerAuth: [] + summary: 获取角色列表 + tags: + - 角色管理 + post: + consumes: + - application/json + description: 创建新角色(仅管理员) + parameters: + - description: 角色信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.CreateRoleRequest' + produces: + - application/json + responses: + "201": + description: 角色创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerRole' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建角色 + tags: + - 角色管理 + /api/v1/roles/{id}: + delete: + description: 删除角色(仅管理员) + parameters: + - description: 角色ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 角色不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除角色 + tags: + - 角色管理 + get: + description: 根据ID获取角色详细信息 + parameters: + - description: 角色ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 角色信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerRole' + type: object + "404": + description: 角色不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取角色详情 + tags: + - 角色管理 + put: + consumes: + - application/json + description: 更新角色信息(仅管理员) + parameters: + - description: 角色ID + in: path + name: id + required: true + type: integer + - description: 更新信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.UpdateRoleRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerRole' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 角色不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新角色 + tags: + - 角色管理 + /api/v1/roles/{id}/permissions: + get: + description: 获取角色的权限列表 + parameters: + - description: 角色ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 权限列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.SwaggerPermission' + type: array + type: object + "404": + description: 角色不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取角色权限列表 + tags: + - 角色管理 + put: + consumes: + - application/json + description: 为角色分配权限(替换现有权限)(仅管理员) + parameters: + - description: 角色ID + in: path + name: id + required: true + type: integer + - description: 权限ID列表 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.AssignPermissionsRequest' + produces: + - application/json + responses: + "200": + description: 权限分配成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 角色不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 分配角色权限 + tags: + - 角色管理 + /api/v1/roles/{id}/status: + put: + consumes: + - application/json + description: 更新角色状态(enabled/disabled)(仅管理员) + parameters: + - description: 角色ID + in: path + name: id + required: true + type: integer + - description: 状态信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdateRoleStatusRequest' + produces: + - application/json + responses: + "200": + description: 状态更新成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 无效的状态值 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 角色不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新角色状态 + tags: + - 角色管理 + /api/v1/sso/authorize: + get: + consumes: + - application/json + description: 处理 SSO 授权请求,返回授权码 + parameters: + - description: 客户端ID + in: query + name: client_id + required: true + type: string + - description: 回调地址 + in: query + name: redirect_uri + required: true + type: string + - description: 响应类型 + enum: + - code + in: query + name: response_type + required: true + type: string + - description: 授权范围 + in: query + name: scope + type: string + - description: 状态参数 + in: query + name: state + type: string + produces: + - application/json + responses: + "302": + description: 重定向到回调地址 + schema: + type: string + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: SSO 授权 + tags: + - SSO + /api/v1/sso/introspect: + post: + consumes: + - application/x-www-form-urlencoded + description: 验证 Access Token 的有效性并返回相关信息 + parameters: + - description: Access Token + in: formData + name: token + required: true + type: string + - description: 客户端ID + in: formData + name: client_id + required: true + type: string + - description: 客户端密钥 + in: formData + name: client_secret + required: true + type: string + produces: + - application/json + responses: + "200": + description: Token信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.IntrospectResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 客户端认证失败 + schema: + $ref: '#/definitions/handler.Response' + summary: 验证 Access Token + tags: + - SSO + /api/v1/sso/revoke: + post: + consumes: + - application/x-www-form-urlencoded + description: 撤销指定的 Access Token + parameters: + - description: Access Token + in: formData + name: token + required: true + type: string + - description: 客户端ID + in: formData + name: client_id + required: true + type: string + - description: 客户端密钥 + in: formData + name: client_secret + required: true + type: string + produces: + - application/json + responses: + "200": + description: 撤销成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 客户端认证失败 + schema: + $ref: '#/definitions/handler.Response' + summary: 撤销 Access Token + tags: + - SSO + /api/v1/sso/token: + post: + consumes: + - application/x-www-form-urlencoded + description: 使用授权码获取 Access Token(授权码模式第二步) + parameters: + - description: 授权类型 + enum: + - authorization_code + in: formData + name: grant_type + required: true + type: string + - description: 授权码 + in: formData + name: code + required: true + type: string + - description: 回调地址 + in: formData + name: redirect_uri + required: true + type: string + - description: 客户端ID + in: formData + name: client_id + required: true + type: string + - description: 客户端密钥 + in: formData + name: client_secret + required: true + type: string + produces: + - application/json + responses: + "200": + description: 访问令牌响应 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.TokenResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 客户端认证失败 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + summary: 获取 Access Token + tags: + - SSO + /api/v1/sso/userinfo: + get: + description: 获取当前通过 SSO Access Token 授权的用户信息 + produces: + - application/json + responses: + "200": + description: 用户信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.UserInfoResponse' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取 SSO 用户信息 + tags: + - SSO + /api/v1/theme/active: + get: + description: 获取当前系统正在使用的主题(公开接口) + produces: + - application/json + responses: + "200": + description: 当前生效主题 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerTheme' + type: object + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + summary: 获取当前生效的主题 + tags: + - 主题管理 + /api/v1/themes: + get: + description: 获取所有主题(包括已禁用的) + produces: + - application/json + responses: + "200": + description: 主题列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.SwaggerTheme' + type: array + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取所有主题 + tags: + - 主题管理 + post: + consumes: + - application/json + description: 创建新的主题配置 + parameters: + - description: 主题信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.CreateThemeRequest' + produces: + - application/json + responses: + "201": + description: 主题创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerTheme' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建主题 + tags: + - 主题管理 + /api/v1/themes/{id}: + delete: + description: 删除指定的主题 + parameters: + - description: 主题ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 主题删除成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除主题 + tags: + - 主题管理 + get: + description: 根据ID获取主题详情 + parameters: + - description: 主题ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 主题详情 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerTheme' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取主题 + tags: + - 主题管理 + put: + consumes: + - application/json + description: 更新指定主题的配置 + parameters: + - description: 主题ID + in: path + name: id + required: true + type: integer + - description: 更新信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.UpdateThemeRequest' + produces: + - application/json + responses: + "200": + description: 主题更新成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerTheme' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新主题 + tags: + - 主题管理 + /api/v1/themes/default: + get: + description: 获取系统默认主题 + produces: + - application/json + responses: + "200": + description: 默认主题 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.SwaggerTheme' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取默认主题 + tags: + - 主题管理 + /api/v1/themes/default/{id}: + put: + description: 将指定主题设为系统默认主题 + parameters: + - description: 主题ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 设置成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 设置默认主题 + tags: + - 主题管理 + /api/v1/users: + get: + description: 获取用户列表,支持游标分页和偏移分页 + parameters: + - description: 游标分页游标 + in: query + name: cursor + type: string + - description: 每页大小 + in: query + name: size + type: integer + - description: 偏移分页偏移量 + in: query + name: offset + type: integer + - description: 每页大小 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 用户列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.UserListResponse' + type: object + security: + - BearerAuth: [] + summary: 获取用户列表 + tags: + - 用户管理 + post: + consumes: + - application/json + description: 创建新用户账号(仅管理员) + parameters: + - description: 用户信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.CreateUserRequest' + produces: + - application/json + responses: + "201": + description: 用户创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.UserResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建用户 + tags: + - 用户管理 + /api/v1/users/{id}: + delete: + description: 删除用户账号(仅管理员) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除用户 + tags: + - 用户管理 + get: + description: 根据ID获取用户详细信息 + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 用户信息 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.UserResponse' + type: object + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取用户详情 + tags: + - 用户管理 + put: + consumes: + - application/json + description: 更新用户的基本信息(仅管理员或本人) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 更新信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdateUserRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.UserResponse' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新用户信息 + tags: + - 用户管理 + /api/v1/users/{id}/avatar: + post: + consumes: + - multipart/form-data + description: 上传并更新用户头像(仅本人或管理员) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 头像文件(最大5MB,支持jpg/jpeg/png/gif/webp) + in: formData + name: avatar + required: true + type: file + produces: + - application/json + responses: + "200": + description: 上传成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.AvatarResponse' + type: object + "400": + description: 文件无效或大小超限 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 上传用户头像 + tags: + - 用户头像 + /api/v1/users/{id}/password: + put: + consumes: + - application/json + description: 修改用户密码(仅管理员或本人) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 密码信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdatePasswordRequest' + produces: + - application/json + responses: + "200": + description: 密码修改成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 修改用户密码 + tags: + - 用户管理 + /api/v1/users/{id}/roles: + get: + description: 获取指定用户的角色列表(仅本人或管理员) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 角色列表 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + items: + $ref: '#/definitions/handler.SwaggerRole' + type: array + type: object + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取用户角色列表 + tags: + - 用户管理 + put: + consumes: + - application/json + description: 为用户分配角色(替换现有角色)(仅管理员) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 角色ID列表 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.AssignRolesRequest' + produces: + - application/json + responses: + "200": + description: 角色分配成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 分配用户角色 + tags: + - 用户管理 + /api/v1/users/{id}/status: + put: + consumes: + - application/json + description: 更新用户账号状态(active/inactive/locked/disabled)(仅管理员) + parameters: + - description: 用户ID + in: path + name: id + required: true + type: integer + - description: 状态信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.UpdateStatusRequest' + produces: + - application/json + responses: + "200": + description: 状态更新成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 无效的状态值 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + "404": + description: 用户不存在 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新用户状态 + tags: + - 用户管理 + /api/v1/users/batch: + delete: + consumes: + - application/json + description: 批量删除多个用户(仅管理员) + parameters: + - description: 批量删除请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.BatchDeleteRequest' + produces: + - application/json + responses: + "200": + description: 批量删除成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 批量删除用户 + tags: + - 用户管理 + /api/v1/users/batch/status: + put: + consumes: + - application/json + description: 批量更新多个用户的状态(仅管理员) + parameters: + - description: 批量更新请求 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.BatchUpdateStatusRequest' + produces: + - application/json + responses: + "200": + description: 批量更新成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "403": + description: 无权限 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 批量更新用户状态 + tags: + - 用户管理 + /api/v1/users/me/bind-email: + delete: + consumes: + - application/json + description: 解绑账号关联的邮箱(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 解绑邮箱 + tags: + - 邮箱绑定 + post: + consumes: + - application/json + description: 使用邮箱验证码绑定账号(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 绑定邮箱 + tags: + - 邮箱绑定 + /api/v1/users/me/bind-email/code: + post: + consumes: + - application/json + description: 发送验证码到邮箱以绑定邮箱(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 发送邮箱绑定验证码 + tags: + - 邮箱绑定 + /api/v1/users/me/bind-phone: + delete: + consumes: + - application/json + description: 解绑账号关联的手机号(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 解绑手机号 + tags: + - 手机绑定 + post: + consumes: + - application/json + description: 使用手机验证码绑定账号(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 绑定手机号 + tags: + - 手机绑定 + /api/v1/users/me/bind-phone/code: + post: + consumes: + - application/json + description: 发送验证码到手机以绑定手机号(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 发送手机绑定验证码 + tags: + - 手机绑定 + /api/v1/users/me/bind-social: + post: + consumes: + - application/json + description: 绑定第三方社交账号到当前用户(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 绑定社交账号 + tags: + - 社交账号 + /api/v1/users/me/bind-social/{provider}: + delete: + consumes: + - application/json + description: 解绑当前用户关联的第三方社交账号(当前未配置) + produces: + - application/json + responses: + "200": + description: 功能未配置 + schema: + $ref: '#/definitions/handler.Response' + summary: 解绑社交账号 + tags: + - 社交账号 + /api/v1/users/me/custom-fields: + get: + description: 获取当前用户的自定义字段值 + produces: + - application/json + responses: + "200": + description: 字段值 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/handler.CustomFieldValuesResponse' + type: object + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取用户自定义字段值 + tags: + - 自定义字段 + put: + consumes: + - application/json + description: 设置当前用户的自定义字段值 + parameters: + - description: 字段值 + in: body + name: request + required: true + schema: + $ref: '#/definitions/handler.SetUserFieldValuesRequest' + produces: + - application/json + responses: + "200": + description: 设置成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 设置用户自定义字段值 + tags: + - 自定义字段 + /api/v1/users/me/social-accounts: + get: + description: 获取当前用户绑定的第三方社交账号列表 + produces: + - application/json + responses: + "200": + description: 社交账号列表 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取已绑定的社交账号列表 + tags: + - 社交账号 + /api/v1/webhooks: + get: + consumes: + - application/json + description: 获取当前用户的 Webhook 配置列表 + parameters: + - default: 1 + description: 页码 + in: query + name: page + type: integer + - default: 20 + description: 每页数量 + in: query + name: page_size + type: integer + produces: + - application/json + responses: + "200": + description: Webhook列表 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取 Webhook 列表 + tags: + - Webhook管理 + post: + consumes: + - application/json + description: 创建新的 Webhook 配置 + parameters: + - description: Webhook信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.CreateWebhookRequest' + produces: + - application/json + responses: + "201": + description: Webhook创建成功 + schema: + allOf: + - $ref: '#/definitions/handler.Response' + - properties: + data: + $ref: '#/definitions/domain.Webhook' + type: object + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 创建 Webhook + tags: + - Webhook管理 + /api/v1/webhooks/{id}: + delete: + description: 删除指定的 Webhook 配置 + parameters: + - description: Webhook ID + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: 删除成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 删除 Webhook + tags: + - Webhook管理 + put: + consumes: + - application/json + description: 更新指定 Webhook 的配置 + parameters: + - description: Webhook ID + in: path + name: id + required: true + type: integer + - description: 更新信息 + in: body + name: request + required: true + schema: + $ref: '#/definitions/service.UpdateWebhookRequest' + produces: + - application/json + responses: + "200": + description: 更新成功 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 更新 Webhook + tags: + - Webhook管理 + /api/v1/webhooks/{id}/deliveries: + get: + description: 获取指定 Webhook 的最近投递记录 + parameters: + - description: Webhook ID + in: path + name: id + required: true + type: integer + - default: 20 + description: 返回记录数量 + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: 投递记录列表 + schema: + $ref: '#/definitions/handler.Response' + "400": + description: 请求参数错误 + schema: + $ref: '#/definitions/handler.Response' + "401": + description: 未认证 + schema: + $ref: '#/definitions/handler.Response' + "500": + description: 服务器错误 + schema: + $ref: '#/definitions/handler.Response' + security: + - BearerAuth: [] + summary: 获取 Webhook 投递记录 + tags: + - Webhook管理 +schemes: +- http +- https +swagger: "2.0" diff --git a/frontend/admin/vite.config.js b/frontend/admin/vite.config.js index aa840cd..a5ac462 100644 --- a/frontend/admin/vite.config.js +++ b/frontend/admin/vite.config.js @@ -9,8 +9,25 @@ const apiProxyTarget = process.env.VITE_API_PROXY_TARGET || 'http://127.0.0.1:80 export default defineConfig({ plugins: [react()], build: { + chunkSizeWarningLimit: 600, rollupOptions: { input: 'index.html', + output: { + manualChunks(id) { + if (id.includes('node_modules')) { + if (id.includes('react-router-dom') || id.includes('/react/') || id.includes('/react-dom/')) { + return 'react-vendor' + } + if (id.includes('/antd/') || id.includes('@ant-design/icons')) { + return 'antd-vendor' + } + if (id.includes('/dayjs/')) { + return 'dayjs-vendor' + } + } + return undefined + }, + }, }, }, resolve: { diff --git a/internal/api/handler/api_contract_integration_test.go b/internal/api/handler/api_contract_integration_test.go index d03449d..d9627ae 100644 --- a/internal/api/handler/api_contract_integration_test.go +++ b/internal/api/handler/api_contract_integration_test.go @@ -368,15 +368,15 @@ func TestProtectedEndpoints_Contract(t *testing.T) { // TestHTTPStatusCodes_Contract 验证 HTTP 状态码使用规范 func TestHTTPStatusCodes_Contract(t *testing.T) { statusCodes := map[int]string{ - http.StatusOK: "成功响应", - http.StatusCreated: "资源创建成功", - http.StatusBadRequest: "请求参数错误", - http.StatusUnauthorized: "未认证", - http.StatusForbidden: "无权限", - http.StatusNotFound: "资源不存在", - http.StatusConflict: "资源冲突", - http.StatusTooManyRequests: "请求过于频繁", - http.StatusInternalServerError: "服务器内部错误", + http.StatusOK: "成功响应", + http.StatusCreated: "资源创建成功", + http.StatusBadRequest: "请求参数错误", + http.StatusUnauthorized: "未认证", + http.StatusForbidden: "无权限", + http.StatusNotFound: "资源不存在", + http.StatusConflict: "资源冲突", + http.StatusTooManyRequests: "请求过于频繁", + http.StatusInternalServerError: "服务器内部错误", } for code, desc := range statusCodes { diff --git a/internal/api/handler/auth_handler.go b/internal/api/handler/auth_handler.go index 81c00d2..f5c88b0 100644 --- a/internal/api/handler/auth_handler.go +++ b/internal/api/handler/auth_handler.go @@ -295,7 +295,7 @@ func (h *AuthHandler) Logout(c *gin.Context) { // @Success 200 {object} Response{data=service.LoginResponse} "刷新成功" // @Failure 400 {object} Response{code=int,message=string} "请求参数错误" // @Failure 401 {object} Response{code=int,message=string} "refresh_token无效或已过期" -// @Router /api/v1/auth/refresh-token [post] +// @Router /api/v1/auth/refresh [post] func (h *AuthHandler) RefreshToken(c *gin.Context) { var req struct { RefreshToken string `json:"refresh_token"` @@ -361,7 +361,7 @@ func (h *AuthHandler) GetUserInfo(c *gin.Context) { // @Description 由于系统使用JWT Bearer Token认证,不存在CSRF风险,返回空token // @Tags 认证 // @Produce json -// @Success 200 {object} map "CSRF token(为空)" +// @Success 200 {object} Response{data=CSRFTokenResponse} "CSRF token(为空)" // @Router /api/v1/auth/csrf-token [get] func (h *AuthHandler) GetCSRFToken(c *gin.Context) { // 系统使用 JWT Bearer Token 认证,Bearer Token 不会被浏览器自动携带(非 cookie) @@ -422,7 +422,7 @@ func (h *AuthHandler) OAuthCallback(c *gin.Context) { // @Produce json // @Param provider path string true "OAuth提供商" // @Success 200 {object} Response "OAuth未配置" -// @Router /api/v1/auth/oauth/{provider}/exchange [post] +// @Router /api/v1/auth/oauth/exchange [post] func (h *AuthHandler) OAuthExchange(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "OAuth exchange is not configured"}) } @@ -432,7 +432,7 @@ func (h *AuthHandler) OAuthExchange(c *gin.Context) { // @Description 返回系统已配置并启用的OAuth提供商列表 // @Tags OAuth // @Produce json -// @Success 200 {object} Response{data=map} "提供商列表" +// @Success 200 {object} Response{data=OAuthProvidersResponse} "提供商列表" // @Router /api/v1/auth/oauth/providers [get] func (h *AuthHandler) GetEnabledOAuthProviders(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"providers": []string{}}}) @@ -471,7 +471,7 @@ func (h *AuthHandler) ActivateEmail(c *gin.Context) { // @Param request body ResendActivationRequest true "邮箱地址" // @Success 200 {object} Response "激活邮件已发送(如果邮箱已注册)" // @Failure 400 {object} Response "邮箱格式错误" -// @Router /api/v1/auth/resend-activation-email [post] +// @Router /api/v1/auth/resend-activation [post] func (h *AuthHandler) ResendActivationEmail(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` @@ -525,7 +525,7 @@ func (h *AuthHandler) SendEmailCode(c *gin.Context) { // @Success 200 {object} Response{data=service.LoginResponse} "登录成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "验证码错误或已过期" -// @Router /api/v1/auth/login-by-email-code [post] +// @Router /api/v1/auth/login/email-code [post] func (h *AuthHandler) LoginByEmailCode(c *gin.Context) { var req struct { Email string `json:"email" binding:"required,email"` @@ -645,7 +645,7 @@ func (h *AuthHandler) BootstrapAdmin(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/email/bind/send [post] +// @Router /api/v1/users/me/bind-email/code [post] func (h *AuthHandler) SendEmailBindCode(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "email binding is not configured"}) } @@ -657,7 +657,7 @@ func (h *AuthHandler) SendEmailBindCode(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/email/bind [post] +// @Router /api/v1/users/me/bind-email [post] func (h *AuthHandler) BindEmail(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "email binding is not configured"}) } @@ -669,7 +669,7 @@ func (h *AuthHandler) BindEmail(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/email/unbind [post] +// @Router /api/v1/users/me/bind-email [delete] func (h *AuthHandler) UnbindEmail(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "email binding is not configured"}) } @@ -681,7 +681,7 @@ func (h *AuthHandler) UnbindEmail(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/phone/bind/send [post] +// @Router /api/v1/users/me/bind-phone/code [post] func (h *AuthHandler) SendPhoneBindCode(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "phone binding is not configured"}) } @@ -693,7 +693,7 @@ func (h *AuthHandler) SendPhoneBindCode(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/phone/bind [post] +// @Router /api/v1/users/me/bind-phone [post] func (h *AuthHandler) BindPhone(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "phone binding is not configured"}) } @@ -705,7 +705,7 @@ func (h *AuthHandler) BindPhone(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/phone/unbind [post] +// @Router /api/v1/users/me/bind-phone [delete] func (h *AuthHandler) UnbindPhone(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "phone binding is not configured"}) } @@ -717,7 +717,7 @@ func (h *AuthHandler) UnbindPhone(c *gin.Context) { // @Produce json // @Security BearerAuth // @Success 200 {object} Response "社交账号列表" -// @Router /api/v1/auth/social-accounts [get] +// @Router /api/v1/users/me/social-accounts [get] func (h *AuthHandler) GetSocialAccounts(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": gin.H{"accounts": []interface{}{}}}) } @@ -729,7 +729,7 @@ func (h *AuthHandler) GetSocialAccounts(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/social/bind [post] +// @Router /api/v1/users/me/bind-social [post] func (h *AuthHandler) BindSocialAccount(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "social binding is not configured"}) } @@ -741,7 +741,7 @@ func (h *AuthHandler) BindSocialAccount(c *gin.Context) { // @Accept json // @Produce json // @Success 200 {object} Response "功能未配置" -// @Router /api/v1/auth/social/unbind [post] +// @Router /api/v1/users/me/bind-social/{provider} [delete] func (h *AuthHandler) UnbindSocialAccount(c *gin.Context) { c.JSON(http.StatusServiceUnavailable, gin.H{"code": http.StatusServiceUnavailable, "message": "social binding is not configured"}) } diff --git a/internal/api/handler/avatar_handler.go b/internal/api/handler/avatar_handler.go index eed6ad4..a649558 100644 --- a/internal/api/handler/avatar_handler.go +++ b/internal/api/handler/avatar_handler.go @@ -169,12 +169,19 @@ func (h *AvatarHandler) UploadAvatar(c *gin.Context) { // Save file to disk dstPath := filepath.Join(uploadDir, avatarFilename) - data := make([]byte, file.Size) - if _, err := src.Read(data); err != nil { + dst, err := os.Create(dstPath) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to save avatar file"}) + return + } + if _, err := io.Copy(dst, src); err != nil { + dst.Close() + os.Remove(dstPath) c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to read uploaded file"}) return } - if err := os.WriteFile(dstPath, data, 0o644); err != nil { + if err := dst.Close(); err != nil { + os.Remove(dstPath) c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to save avatar file"}) return } @@ -202,9 +209,9 @@ func (h *AvatarHandler) UploadAvatar(c *gin.Context) { c.JSON(http.StatusOK, gin.H{ "code": 0, "message": "avatar uploaded successfully", - "data": gin.H{ - "avatar_url": avatarURL, - "thumbnail": avatarURL, + "data": AvatarResponse{ + AvatarURL: avatarURL, + Thumbnail: avatarURL, }, }) } diff --git a/internal/api/handler/avatar_handler_test.go b/internal/api/handler/avatar_handler_test.go index af56f68..ddaceeb 100644 --- a/internal/api/handler/avatar_handler_test.go +++ b/internal/api/handler/avatar_handler_test.go @@ -39,7 +39,7 @@ func doUploadAvatar(url, token string, userID string, filename string, content [ // Create multipart form var body bytes.Buffer writer := multipart.NewWriter(&body) - + // Add file part, _ := writer.CreateFormFile("avatar", filename) part.Write(content) @@ -76,7 +76,7 @@ func TestAvatarHandler_UploadAvatar_Success(t *testing.T) { // Get user ID by getting user info resp, body := doGet(server.URL+"/api/v1/users/me", token) defer resp.Body.Close() - + userID := "1" // Default to 1, adjust based on response if resp.StatusCode == http.StatusOK { // Parse user ID from response @@ -129,7 +129,7 @@ func TestAvatarHandler_UploadAvatar_OtherUser_Forbidden(t *testing.T) { registerUser(server.URL, "usera", "usera@test.com", "Pass123!") tokenA := getToken(server.URL, "usera", "Pass123!") - + registerUser(server.URL, "userb", "userb@test.com", "Pass123!") // userB token - but we try to upload to userA @@ -200,7 +200,10 @@ func TestAvatarHandler_UploadAvatar_NoFile(t *testing.T) { req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} - resp, _ := client.Do(req) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } defer resp.Body.Close() // Should reject missing file @@ -220,7 +223,7 @@ func TestAvatarHandler_UploadAvatar_FileTooLarge(t *testing.T) { // Create oversized file (6MB > 5MB limit) largeContent := make([]byte, 6*1024*1024) copy(largeContent, []byte{0x89, 0x50, 0x4E, 0x47}) // PNG header - + resp, _ := doUploadAvatar(server.URL, token, "1", "large.png", largeContent) defer resp.Body.Close() @@ -239,7 +242,7 @@ func TestAvatarHandler_UploadAvatar_AllowedFormats(t *testing.T) { assert.NotEmpty(t, token) formats := []string{".png", ".jpg", ".jpeg", ".gif", ".webp"} - + for i, ext := range formats { imageData := createTestImage(ext) // Ensure we don't slice beyond the length @@ -248,9 +251,9 @@ func TestAvatarHandler_UploadAvatar_AllowedFormats(t *testing.T) { dataSize = 100 } resp, respBody := doUploadAvatar(server.URL, token, "1", "avatar"+ext, imageData[:dataSize]) - + t.Logf("Format %s returned status: %d", ext, resp.StatusCode) - + // Accept various responses based on image validity if i == len(formats)-1 { resp.Body.Close() @@ -269,12 +272,12 @@ func TestAvatarHandler_UploadAvatar_DisallowedExtensions(t *testing.T) { assert.NotEmpty(t, token) disallowed := []string{".exe", ".php", ".sh", ".bat", ".pdf", ".doc"} - + for _, ext := range disallowed { fakeContent := []byte("fake content") resp, _ := doUploadAvatar(server.URL, token, "1", "file"+ext, fakeContent) defer resp.Body.Close() - + // Should reject disallowed extensions if resp.StatusCode != http.StatusOK { assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusInternalServerError, diff --git a/internal/api/handler/captcha_handler.go b/internal/api/handler/captcha_handler.go index 9bc4ffb..216d7ce 100644 --- a/internal/api/handler/captcha_handler.go +++ b/internal/api/handler/captcha_handler.go @@ -24,7 +24,7 @@ func NewCaptchaHandler(captchaService *service.CaptchaService) *CaptchaHandler { // @Tags 验证码 // @Produce json // @Success 200 {object} Response{data=CaptchaResponse} "验证码信息" -// @Router /api/v1/captcha/generate [get] +// @Router /api/v1/auth/captcha [get] func (h *CaptchaHandler) GenerateCaptcha(c *gin.Context) { result, err := h.captchaService.Generate(c.Request.Context()) if err != nil { @@ -49,7 +49,7 @@ func (h *CaptchaHandler) GenerateCaptcha(c *gin.Context) { // @Produce json // @Param captcha_id query string false "验证码ID" // @Success 200 {object} Response "验证码图片" -// @Router /api/v1/captcha/image [get] +// @Router /api/v1/auth/captcha/image [get] func (h *CaptchaHandler) GetCaptchaImage(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"}) } @@ -63,7 +63,7 @@ func (h *CaptchaHandler) GetCaptchaImage(c *gin.Context) { // @Param request body VerifyCaptchaRequest true "验证码信息" // @Success 200 {object} Response{data=VerifyResponse} "验证成功" // @Failure 400 {object} Response "验证码无效" -// @Router /api/v1/captcha/verify [post] +// @Router /api/v1/auth/captcha/verify [post] func (h *CaptchaHandler) VerifyCaptcha(c *gin.Context) { var req struct { CaptchaID string `json:"captcha_id" binding:"required"` diff --git a/internal/api/handler/context_guard_test.go b/internal/api/handler/context_guard_test.go index b5352a2..b71a42f 100644 --- a/internal/api/handler/context_guard_test.go +++ b/internal/api/handler/context_guard_test.go @@ -8,6 +8,7 @@ import ( "testing" "github.com/gin-gonic/gin" + "github.com/user-management-system/internal/auth" ) func init() { @@ -15,7 +16,14 @@ func init() { } func TestSSOHandlerAuthorize_InvalidContextTypes_ReturnsUnauthorized(t *testing.T) { - h := &SSOHandler{} + h := &SSOHandler{clientsStore: auth.NewDefaultSSOClientsStore()} + store := h.clientsStore.(*auth.DefaultSSOClientsStore) + store.RegisterClient(&auth.SSOClient{ + ClientID: "test-client", + ClientSecret: "test-secret", + RedirectURIs: []string{"https://example.com/callback"}, + }) + engine := gin.New() engine.GET("/authorize", func(c *gin.Context) { c.Set("user_id", "not-int64") diff --git a/internal/api/handler/custom_field_handler.go b/internal/api/handler/custom_field_handler.go index 77cd4cd..b4e6b9c 100644 --- a/internal/api/handler/custom_field_handler.go +++ b/internal/api/handler/custom_field_handler.go @@ -27,10 +27,10 @@ func NewCustomFieldHandler(customFieldService *service.CustomFieldService) *Cust // @Produce json // @Security BearerAuth // @Param request body service.CreateFieldRequest true "字段定义" -// @Success 201 {object} Response{data=domain.CustomField} "创建成功" +// @Success 201 {object} Response{data=SwaggerCustomField} "创建成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" -// @Router /api/v1/fields [post] +// @Router /api/v1/custom-fields [post] func (h *CustomFieldHandler) CreateField(c *gin.Context) { var req service.CreateFieldRequest if err := c.ShouldBindJSON(&req); err != nil { @@ -60,11 +60,11 @@ func (h *CustomFieldHandler) CreateField(c *gin.Context) { // @Security BearerAuth // @Param id path int true "字段ID" // @Param request body service.UpdateFieldRequest true "更新信息" -// @Success 200 {object} Response{data=domain.CustomField} "更新成功" +// @Success 200 {object} Response{data=SwaggerCustomField} "更新成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" // @Failure 404 {object} Response "字段不存在" -// @Router /api/v1/fields/{id} [put] +// @Router /api/v1/custom-fields/{id} [put] func (h *CustomFieldHandler) UpdateField(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { @@ -101,7 +101,7 @@ func (h *CustomFieldHandler) UpdateField(c *gin.Context) { // @Success 200 {object} Response "删除成功" // @Failure 403 {object} Response "无权限" // @Failure 404 {object} Response "字段不存在" -// @Router /api/v1/fields/{id} [delete] +// @Router /api/v1/custom-fields/{id} [delete] func (h *CustomFieldHandler) DeleteField(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { @@ -127,9 +127,9 @@ func (h *CustomFieldHandler) DeleteField(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "字段ID" -// @Success 200 {object} Response{data=domain.CustomField} "字段信息" +// @Success 200 {object} Response{data=SwaggerCustomField} "字段信息" // @Failure 404 {object} Response "字段不存在" -// @Router /api/v1/fields/{id} [get] +// @Router /api/v1/custom-fields/{id} [get] func (h *CustomFieldHandler) GetField(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { @@ -156,8 +156,8 @@ func (h *CustomFieldHandler) GetField(c *gin.Context) { // @Tags 自定义字段 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=[]domain.CustomField} "字段列表" -// @Router /api/v1/fields [get] +// @Success 200 {object} Response{data=[]SwaggerCustomField} "字段列表" +// @Router /api/v1/custom-fields [get] func (h *CustomFieldHandler) ListFields(c *gin.Context) { fields, err := h.customFieldService.ListFields(c.Request.Context()) if err != nil { @@ -183,7 +183,7 @@ func (h *CustomFieldHandler) ListFields(c *gin.Context) { // @Success 200 {object} Response "设置成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/users/me/fields [put] +// @Router /api/v1/users/me/custom-fields [put] func (h *CustomFieldHandler) SetUserFieldValues(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -217,9 +217,9 @@ func (h *CustomFieldHandler) SetUserFieldValues(c *gin.Context) { // @Tags 自定义字段 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=map} "字段值" +// @Success 200 {object} Response{data=CustomFieldValuesResponse} "字段值" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/users/me/fields [get] +// @Router /api/v1/users/me/custom-fields [get] func (h *CustomFieldHandler) GetUserFieldValues(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { diff --git a/internal/api/handler/custom_field_handler_test.go b/internal/api/handler/custom_field_handler_test.go index 4f48215..d1d6b4c 100644 --- a/internal/api/handler/custom_field_handler_test.go +++ b/internal/api/handler/custom_field_handler_test.go @@ -31,7 +31,7 @@ func TestCustomFieldHandler_CreateField_Success(t *testing.T) { }) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusForbidden || + assert.True(t, resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusInternalServerError, "should create field, got %d: %s", resp.StatusCode, body) } @@ -138,7 +138,7 @@ func TestCustomFieldHandler_GetField_NotFound(t *testing.T) { resp, _ := doGet(server.URL+"/api/v1/fields/99999", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest || + assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK, "should handle NotFound, got %d", resp.StatusCode) } @@ -156,7 +156,7 @@ func TestCustomFieldHandler_GetField_InvalidID(t *testing.T) { resp, _ := doGet(server.URL+"/api/v1/fields/invalid", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusNotFound || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK, "should handle InvalidID, got %d", resp.StatusCode) } @@ -204,7 +204,7 @@ func TestCustomFieldHandler_UpdateField_NotFound(t *testing.T) { }) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest || + assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK, "should handle NotFound, got %d", resp.StatusCode) } @@ -265,7 +265,7 @@ func TestCustomFieldHandler_DeleteField_NotFound(t *testing.T) { resp, _ := doDelete(server.URL+"/api/v1/fields/99999", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest || + assert.True(t, resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK, "should handle NotFound, got %d", resp.StatusCode) } @@ -283,7 +283,7 @@ func TestCustomFieldHandler_DeleteField_InvalidID(t *testing.T) { resp, _ := doDelete(server.URL+"/api/v1/fields/invalid", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusNotFound || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK, "should handle InvalidID, got %d", resp.StatusCode) } @@ -385,7 +385,7 @@ func TestCustomFieldHandler_FieldTypes_Support(t *testing.T) { fieldTypes := []string{"text", "number", "date", "boolean", "select"} for _, ft := range fieldTypes { resp, _ := doPost(server.URL+"/api/v1/fields", token, map[string]interface{}{ - "name": "field_" + ft, + "name": "field_" + ft, "label": "Field " + ft, "type": ft, }) diff --git a/internal/api/handler/device_handler.go b/internal/api/handler/device_handler.go index 7c80fc6..6f1d908 100644 --- a/internal/api/handler/device_handler.go +++ b/internal/api/handler/device_handler.go @@ -31,7 +31,7 @@ func NewDeviceHandler(deviceService *service.DeviceService) *DeviceHandler { // @Produce json // @Security BearerAuth // @Param request body service.CreateDeviceRequest true "设备信息" -// @Success 201 {object} Response{data=domain.Device} "设备创建成功" +// @Success 201 {object} Response{data=SwaggerDevice} "设备创建成功" // @Failure 401 {object} Response "未认证" // @Router /api/v1/devices [post] func (h *DeviceHandler) CreateDevice(c *gin.Context) { @@ -109,7 +109,7 @@ func (h *DeviceHandler) GetMyDevices(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "设备ID" -// @Success 200 {object} Response{data=domain.Device} "设备信息" +// @Success 200 {object} Response{data=SwaggerDevice} "设备信息" // @Failure 404 {object} Response "设备不存在" // @Router /api/v1/devices/{id} [get] func (h *DeviceHandler) GetDevice(c *gin.Context) { @@ -140,7 +140,7 @@ func (h *DeviceHandler) GetDevice(c *gin.Context) { // @Security BearerAuth // @Param id path int true "设备ID" // @Param request body service.UpdateDeviceRequest true "更新信息" -// @Success 200 {object} Response{data=domain.Device} "更新成功" +// @Success 200 {object} Response{data=SwaggerDevice} "更新成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 404 {object} Response "设备不存在" // @Router /api/v1/devices/{id} [put] @@ -245,6 +245,7 @@ func (h *DeviceHandler) UpdateDeviceStatus(c *gin.Context) { status = domain.DeviceStatusActive case "inactive", "0": status = domain.DeviceStatusInactive + default: c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid status"}) return @@ -272,7 +273,7 @@ func (h *DeviceHandler) UpdateDeviceStatus(c *gin.Context) { // @Param page_size query int false "每页数量" // @Success 200 {object} Response{data=DeviceListResponse} "设备列表" // @Failure 403 {object} Response "无权限" -// @Router /api/v1/users/{id}/devices [get] +// @Router /api/v1/devices/users/{id} [get] func (h *DeviceHandler) GetUserDevices(c *gin.Context) { // IDOR 修复:检查当前用户是否有权限查看指定用户的设备 currentUserID, ok := getUserIDFromContext(c) @@ -430,7 +431,7 @@ func (h *DeviceHandler) TrustDevice(c *gin.Context) { // @Param request body TrustDeviceRequest true "信任配置" // @Success 200 {object} Response "设置成功" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/devices/trust/{deviceId} [post] +// @Router /api/v1/devices/by-device-id/{deviceId}/trust [post] func (h *DeviceHandler) TrustDeviceByDeviceID(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -502,9 +503,9 @@ func (h *DeviceHandler) UntrustDevice(c *gin.Context) { // @Tags 设备管理 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=[]domain.Device} "信任设备列表" +// @Success 200 {object} Response{data=[]SwaggerDevice} "信任设备列表" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/devices/trusted [get] +// @Router /api/v1/devices/me/trusted [get] func (h *DeviceHandler) GetMyTrustedDevices(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -535,7 +536,7 @@ func (h *DeviceHandler) GetMyTrustedDevices(c *gin.Context) { // @Success 200 {object} Response "登出成功" // @Failure 400 {object} Response "无效的设备ID" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/devices/logout-others [post] +// @Router /api/v1/devices/me/logout-others [post] func (h *DeviceHandler) LogoutAllOtherDevices(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { diff --git a/internal/api/handler/export_handler.go b/internal/api/handler/export_handler.go index ee76458..b658cd8 100644 --- a/internal/api/handler/export_handler.go +++ b/internal/api/handler/export_handler.go @@ -27,14 +27,14 @@ func NewExportHandler(exportService *service.ExportService) *ExportHandler { // @Accept json // @Produce json // @Security BearerAuth -// @Param format query string false "导出格式" default(csv) Enums(csv, excel) +// @Param format query string false "导出格式" default(csv) Enums(csv, xlsx) // @Param fields query string false "导出字段,逗号分隔" // @Param keyword query string false "关键词过滤" // @Param status query int false "用户状态过滤" // @Success 200 {file} file "用户数据文件" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/exports/users [get] +// @Router /api/v1/admin/users/export [get] func (h *ExportHandler) ExportUsers(c *gin.Context) { format := c.DefaultQuery("format", "csv") fieldsStr := c.Query("fields") @@ -49,9 +49,11 @@ func (h *ExportHandler) ExportUsers(c *gin.Context) { var status *int if statusStr != "" { s, err := strconvAtoi(statusStr) - if err == nil { - status = &s + if err != nil { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid status"}) + return } + status = &s } req := &service.ExportUsersRequest{ @@ -81,12 +83,12 @@ func (h *ExportHandler) ExportUsers(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param file formData file true "导入文件" -// @Param format query string false "文件格式" default(csv) Enums(csv, excel) +// @Param format query string false "文件格式" default(csv) Enums(csv, xlsx) // @Success 200 {object} Response "导入结果" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/exports/users [post] +// @Router /api/v1/admin/users/import [post] func (h *ExportHandler) ImportUsers(c *gin.Context) { file, _, err := c.Request.FormFile("file") if err != nil { @@ -120,11 +122,11 @@ func (h *ExportHandler) ImportUsers(c *gin.Context) { // @Tags 数据导入导出 // @Produce json // @Security BearerAuth -// @Param format query string false "模板格式" default(csv) Enums(csv, excel) +// @Param format query string false "模板格式" default(csv) Enums(csv, xlsx) // @Success 200 {file} file "导入模板文件" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/exports/template [get] +// @Router /api/v1/admin/users/import/template [get] func (h *ExportHandler) GetImportTemplate(c *gin.Context) { format := c.DefaultQuery("format", "csv") data, filename, contentType, err := h.exportService.GetImportTemplateByFormat(format) @@ -139,10 +141,13 @@ func (h *ExportHandler) GetImportTemplate(c *gin.Context) { } func strconvAtoi(s string) (int, error) { + if s == "" { + return 0, http.ErrNoLocation + } var n int for _, c := range s { if c < '0' || c > '9' { - return 0, nil + return 0, http.ErrNotSupported } n = n*10 + int(c-'0') } diff --git a/internal/api/handler/export_handler_test.go b/internal/api/handler/export_handler_test.go index 513e746..0c76f4f 100644 --- a/internal/api/handler/export_handler_test.go +++ b/internal/api/handler/export_handler_test.go @@ -24,7 +24,7 @@ func TestExportHandler_ExportUsers_Success(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/users", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/export", token) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusInternalServerError, @@ -42,16 +42,17 @@ func TestExportHandler_ExportUsers_WithFormat(t *testing.T) { } // CSV format - resp1, _ := doGet(server.URL+"/api/v1/exports/users?format=csv", token) + resp1, _ := doGet(server.URL+"/api/v1/admin/users/export?format=csv", token) defer resp1.Body.Close() assert.True(t, resp1.StatusCode == http.StatusOK || resp1.StatusCode == http.StatusForbidden, "should export CSV, got %d", resp1.StatusCode) - // Excel format - resp2, _ := doGet(server.URL+"/api/v1/exports/users?format=excel", token) + // XLSX format + resp2, _ := doGet(server.URL+"/api/v1/admin/users/export?format=xlsx", token) defer resp2.Body.Close() assert.True(t, resp2.StatusCode == http.StatusOK || resp2.StatusCode == http.StatusForbidden || resp2.StatusCode == http.StatusBadRequest, - "should export Excel, got %d", resp2.StatusCode) + "should export XLSX, got %d", resp2.StatusCode) + } // TestExportHandler_ExportUsers_WithFields 验证指定字段导出 @@ -64,7 +65,7 @@ func TestExportHandler_ExportUsers_WithFields(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/users?fields=id,username,email&format=csv", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/export?fields=id,username,email&format=csv", token) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden, @@ -81,13 +82,29 @@ func TestExportHandler_ExportUsers_WithFilter(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/users?keyword=admin&status=1&format=csv", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/export?keyword=admin&status=1&format=csv", token) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest, "should export with filter, got %d", resp.StatusCode) } +// TestExportHandler_ExportUsers_InvalidStatus 验证非法状态参数 +func TestExportHandler_ExportUsers_InvalidStatus(t *testing.T) { + server, cleanup := setupHandlerTestServer(t) + defer cleanup() + + token := bootstrapAdminToken(server.URL, "admin", "admin@test.com", "AdminPass123!") + if token == "" { + t.Fatal("bootstrap admin token should succeed") + } + + resp, _ := doGet(server.URL+"/api/v1/admin/users/export?status=abc&format=csv", token) + defer resp.Body.Close() + + assert.Equal(t, http.StatusBadRequest, resp.StatusCode) +} + // TestExportHandler_ExportUsers_NonAdmin 验证非管理员导出 func TestExportHandler_ExportUsers_NonAdmin(t *testing.T) { server, cleanup := setupHandlerTestServer(t) @@ -97,7 +114,7 @@ func TestExportHandler_ExportUsers_NonAdmin(t *testing.T) { token := getToken(server.URL, "regular", "Pass123!") assert.NotEmpty(t, token) - resp, _ := doGet(server.URL+"/api/v1/exports/users", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/export", token) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK, @@ -109,7 +126,7 @@ func TestExportHandler_ExportUsers_Unauthorized(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() - resp, _ := doGet(server.URL+"/api/v1/exports/users", "") + resp, _ := doGet(server.URL+"/api/v1/admin/users/export", "") defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden, @@ -134,7 +151,7 @@ func TestExportHandler_ImportUsers_Success(t *testing.T) { part.Write([]byte(csvData)) writer.Close() - req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users?format=csv", &body) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/admin/users/import?format=csv", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+token) @@ -165,16 +182,20 @@ func TestExportHandler_ImportUsers_NoFile(t *testing.T) { writer := multipart.NewWriter(&body) writer.Close() - req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users", &body) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/admin/users/import", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} - resp, _ := client.Do(req) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusOK, "should require file, got %d", resp.StatusCode) + } // TestExportHandler_ImportUsers_InvalidFormat 验证无效格式导入 @@ -193,16 +214,20 @@ func TestExportHandler_ImportUsers_InvalidFormat(t *testing.T) { part.Write([]byte("invalid content")) writer.Close() - req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users?format=invalid", &body) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/admin/users/import?format=invalid", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} - resp, _ := client.Do(req) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden, "should handle invalid format, got %d", resp.StatusCode) + } // TestExportHandler_ImportUsers_NonAdmin 验证非管理员导入 @@ -220,12 +245,15 @@ func TestExportHandler_ImportUsers_NonAdmin(t *testing.T) { part.Write([]byte("username,email\nuser1,user1@test.com")) writer.Close() - req, _ := http.NewRequest("POST", server.URL+"/api/v1/exports/users", &body) + req, _ := http.NewRequest("POST", server.URL+"/api/v1/admin/users/import", &body) req.Header.Set("Content-Type", writer.FormDataContentType()) req.Header.Set("Authorization", "Bearer "+token) client := &http.Client{} - resp, _ := client.Do(req) + resp, err := client.Do(req) + if err != nil { + t.Fatalf("request failed: %v", err) + } defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK, @@ -242,7 +270,7 @@ func TestExportHandler_GetImportTemplate_Success(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/template", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/import/template", token) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusInternalServerError, @@ -259,7 +287,7 @@ func TestExportHandler_GetImportTemplate_CSV(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/template?format=csv", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/import/template?format=csv", token) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden, @@ -276,11 +304,11 @@ func TestExportHandler_GetImportTemplate_Excel(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/template?format=excel", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/import/template?format=xlsx", token) defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusBadRequest, - "should get Excel template, got %d", resp.StatusCode) + "should get XLSX template, got %d", resp.StatusCode) } // TestExportHandler_GetImportTemplate_Unauthorized 验证未认证获取模板 @@ -288,7 +316,7 @@ func TestExportHandler_GetImportTemplate_Unauthorized(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() - resp, _ := doGet(server.URL+"/api/v1/exports/template", "") + resp, _ := doGet(server.URL+"/api/v1/admin/users/import/template", "") defer resp.Body.Close() assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden, @@ -305,7 +333,7 @@ func TestExportHandler_ExportResponse_ContentType(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/users?format=csv", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/export?format=csv", token) defer resp.Body.Close() if resp.StatusCode == http.StatusOK { @@ -325,7 +353,7 @@ func TestExportHandler_ExportResponse_ContentDisposition(t *testing.T) { t.Fatal("bootstrap admin token should succeed") } - resp, _ := doGet(server.URL+"/api/v1/exports/users?format=csv", token) + resp, _ := doGet(server.URL+"/api/v1/admin/users/export?format=csv", token) defer resp.Body.Close() if resp.StatusCode == http.StatusOK { diff --git a/internal/api/handler/handler_test.go b/internal/api/handler/handler_test.go index 65943d3..5f0592a 100644 --- a/internal/api/handler/handler_test.go +++ b/internal/api/handler/handler_test.go @@ -120,6 +120,8 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) { opLogSvc := service.NewOperationLogService(opLogRepo) webhookSvc := service.NewWebhookService(db) captchaSvc := service.NewCaptchaService(cacheManager) + exportSvc := service.NewExportService(userRepo, roleRepo) + totpSvc := service.NewTOTPService(userRepo) pwdResetCfg := service.DefaultPasswordResetConfig() pwdResetSvc := service.NewPasswordResetService(userRepo, cacheManager, pwdResetCfg). @@ -128,6 +130,15 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) { themeSvc := service.NewThemeService(themeRepo) avatarH := handler.NewAvatarHandler(userRepo) + ssoManager := auth.NewSSOManager() + ssoClientsStore := auth.NewDefaultSSOClientsStore() + ssoClientsStore.RegisterClient(&auth.SSOClient{ + ClientID: "test-client", + ClientSecret: "test-secret", + Name: "Handler Test Client", + RedirectURIs: []string{"http://localhost/callback"}, + }) + ssoH := handler.NewSSOHandler(ssoManager, ssoClientsStore) rateLimitCfg := config.RateLimitConfig{} rateLimitMiddleware := middleware.NewRateLimitMiddleware(rateLimitCfg) authMiddleware := middleware.NewAuthMiddleware( @@ -147,12 +158,13 @@ func setupHandlerTestServer(t *testing.T) (*httptest.Server, func()) { totpHandler := handler.NewTOTPHandler(authSvc, totpSvc) pwdResetHandler := handler.NewPasswordResetHandler(pwdResetSvc) themeHandler := handler.NewThemeHandler(themeSvc) + exportHandler := handler.NewExportHandler(exportSvc) r := router.NewRouter( authHandler, userHandler, roleHandler, permHandler, deviceHandler, logHandler, authMiddleware, rateLimitMiddleware, opLogMiddleware, pwdResetHandler, captchaHandler, totpHandler, webhookHandler, - nil, nil, nil, nil, nil, themeHandler, nil, nil, nil, avatarH, + nil, exportHandler, nil, nil, nil, themeHandler, ssoH, nil, nil, avatarH, ) engine := r.Setup() diff --git a/internal/api/handler/log_handler.go b/internal/api/handler/log_handler.go index 65c2092..12f22df 100644 --- a/internal/api/handler/log_handler.go +++ b/internal/api/handler/log_handler.go @@ -34,7 +34,7 @@ func NewLogHandler(loginLogService *service.LoginLogService, operationLogService // @Param page_size query int false "每页数量" // @Success 200 {object} Response{data=LoginLogListResponse} "登录日志列表" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/users/me/login-logs [get] +// @Router /api/v1/logs/login/me [get] func (h *LogHandler) GetMyLoginLogs(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -76,7 +76,7 @@ func (h *LogHandler) GetMyLoginLogs(c *gin.Context) { // @Param page_size query int false "每页数量" // @Success 200 {object} Response{data=OperationLogListResponse} "操作日志列表" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/users/me/operation-logs [get] +// @Router /api/v1/logs/operation/me [get] func (h *LogHandler) GetMyOperationLogs(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -120,7 +120,7 @@ func (h *LogHandler) GetMyOperationLogs(c *gin.Context) { // @Param page_size query int false "每页数量" // @Success 200 {object} Response{data=LoginLogListResponse} "登录日志列表" // @Failure 403 {object} Response "无权限" -// @Router /api/v1/admin/logs/login [get] +// @Router /api/v1/logs/login [get] func (h *LogHandler) GetLoginLogs(c *gin.Context) { var req service.ListLoginLogRequest if err := c.ShouldBindQuery(&req); err != nil { @@ -175,7 +175,7 @@ func (h *LogHandler) GetLoginLogs(c *gin.Context) { // @Success 200 {object} Response{data=OperationLogListResponse} "操作日志列表" // @Failure 403 {object} Response "无权限" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/admin/logs/operation [get] +// @Router /api/v1/logs/operation [get] func (h *LogHandler) GetOperationLogs(c *gin.Context) { var req service.ListOperationLogRequest if err := c.ShouldBindQuery(&req); err != nil { @@ -229,7 +229,7 @@ func (h *LogHandler) GetOperationLogs(c *gin.Context) { // @Success 200 {file} file "CSV文件" // @Failure 403 {object} Response "无权限" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/admin/logs/login/export [get] +// @Router /api/v1/logs/login/export [get] func (h *LogHandler) ExportLoginLogs(c *gin.Context) { var req service.ExportLoginLogRequest if err := c.ShouldBindQuery(&req); err != nil { diff --git a/internal/api/handler/password_reset_handler.go b/internal/api/handler/password_reset_handler.go index 9dc5653..5cffaa6 100644 --- a/internal/api/handler/password_reset_handler.go +++ b/internal/api/handler/password_reset_handler.go @@ -41,7 +41,7 @@ type ValidateResetTokenRequest struct { // @Param request body ForgotPasswordRequest true "邮箱地址" // @Success 200 {object} Response "密码重置邮件已发送" // @Failure 400 {object} Response "请求参数错误" -// @Router /api/v1/auth/password/forgot [post] +// @Router /api/v1/auth/forgot-password [post] func (h *PasswordResetHandler) ForgotPassword(c *gin.Context) { var req struct { Email string `json:"email" binding:"required"` @@ -95,7 +95,7 @@ func (h *PasswordResetHandler) ValidateResetToken(c *gin.Context) { // @Param request body ResetPasswordRequest true "重置请求" // @Success 200 {object} Response "密码重置成功" // @Failure 400 {object} Response "请求参数错误" -// @Router /api/v1/auth/password/reset [post] +// @Router /api/v1/auth/reset-password [post] func (h *PasswordResetHandler) ResetPassword(c *gin.Context) { var req struct { Token string `json:"token" binding:"required"` @@ -130,7 +130,7 @@ type ForgotPasswordByPhoneRequest struct { // @Success 200 {object} Response "验证码发送成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 503 {object} Response "短信服务未配置" -// @Router /api/v1/auth/password/sms/forgot [post] +// @Router /api/v1/auth/forgot-password/phone [post] func (h *PasswordResetHandler) ForgotPasswordByPhone(c *gin.Context) { if h.smsService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS service not configured"}) @@ -187,7 +187,7 @@ type ResetPasswordByPhoneRequest struct { // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "验证码错误" // @Failure 503 {object} Response "短信服务未配置" -// @Router /api/v1/auth/password/sms/reset [post] +// @Router /api/v1/auth/reset-password/phone [post] func (h *PasswordResetHandler) ResetPasswordByPhone(c *gin.Context) { var req ResetPasswordByPhoneRequest if err := c.ShouldBindJSON(&req); err != nil { diff --git a/internal/api/handler/permission_handler.go b/internal/api/handler/permission_handler.go index 751c19f..e256d61 100644 --- a/internal/api/handler/permission_handler.go +++ b/internal/api/handler/permission_handler.go @@ -28,7 +28,7 @@ func NewPermissionHandler(permissionService *service.PermissionService) *Permiss // @Produce json // @Security BearerAuth // @Param request body service.CreatePermissionRequest true "权限信息" -// @Success 201 {object} Response{data=domain.Permission} "创建成功" +// @Success 201 {object} Response{data=SwaggerPermission} "创建成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" // @Router /api/v1/permissions [post] @@ -58,7 +58,7 @@ func (h *PermissionHandler) CreatePermission(c *gin.Context) { // @Tags 权限管理 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=[]domain.Permission} "权限列表" +// @Success 200 {object} Response{data=[]SwaggerPermission} "权限列表" // @Router /api/v1/permissions [get] func (h *PermissionHandler) ListPermissions(c *gin.Context) { var req service.ListPermissionRequest @@ -87,7 +87,7 @@ func (h *PermissionHandler) ListPermissions(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "权限ID" -// @Success 200 {object} Response{data=domain.Permission} "权限信息" +// @Success 200 {object} Response{data=SwaggerPermission} "权限信息" // @Failure 404 {object} Response "权限不存在" // @Router /api/v1/permissions/{id} [get] func (h *PermissionHandler) GetPermission(c *gin.Context) { @@ -119,7 +119,7 @@ func (h *PermissionHandler) GetPermission(c *gin.Context) { // @Security BearerAuth // @Param id path int true "权限ID" // @Param request body service.UpdatePermissionRequest true "更新信息" -// @Success 200 {object} Response{data=domain.Permission} "更新成功" +// @Success 200 {object} Response{data=SwaggerPermission} "更新成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" // @Failure 404 {object} Response "权限不存在" @@ -237,7 +237,7 @@ func (h *PermissionHandler) UpdatePermissionStatus(c *gin.Context) { // @Tags 权限管理 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=[]domain.Permission} "权限树" +// @Success 200 {object} Response{data=[]SwaggerPermission} "权限树" // @Router /api/v1/permissions/tree [get] func (h *PermissionHandler) GetPermissionTree(c *gin.Context) { tree, err := h.permissionService.GetPermissionTree(c.Request.Context()) diff --git a/internal/api/handler/rbac_handler_test.go b/internal/api/handler/rbac_handler_test.go index 9a3b328..126e9e1 100644 --- a/internal/api/handler/rbac_handler_test.go +++ b/internal/api/handler/rbac_handler_test.go @@ -663,7 +663,7 @@ func TestPermissionHandler_DeletePermission_Success(t *testing.T) { // If creation succeeded, try to delete if permID > 0 { - resp2, _ := doDelete(server.URL + "/api/v1/permissions/" + strconv.Itoa(permID), token) + resp2, _ := doDelete(server.URL+"/api/v1/permissions/"+strconv.Itoa(permID), token) defer resp2.Body.Close() assert.Equal(t, http.StatusOK, resp2.StatusCode, "should delete permission") } diff --git a/internal/api/handler/role_handler.go b/internal/api/handler/role_handler.go index 42dabbb..1e18e80 100644 --- a/internal/api/handler/role_handler.go +++ b/internal/api/handler/role_handler.go @@ -28,7 +28,7 @@ func NewRoleHandler(roleService *service.RoleService) *RoleHandler { // @Produce json // @Security BearerAuth // @Param request body service.CreateRoleRequest true "角色信息" -// @Success 201 {object} Response{data=domain.Role} "角色创建成功" +// @Success 201 {object} Response{data=SwaggerRole} "角色创建成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" // @Router /api/v1/roles [post] @@ -90,7 +90,7 @@ func (h *RoleHandler) ListRoles(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "角色ID" -// @Success 200 {object} Response{data=domain.Role} "角色信息" +// @Success 200 {object} Response{data=SwaggerRole} "角色信息" // @Failure 404 {object} Response "角色不存在" // @Router /api/v1/roles/{id} [get] func (h *RoleHandler) GetRole(c *gin.Context) { @@ -122,7 +122,7 @@ func (h *RoleHandler) GetRole(c *gin.Context) { // @Security BearerAuth // @Param id path int true "角色ID" // @Param request body service.UpdateRoleRequest true "更新信息" -// @Success 200 {object} Response{data=domain.Role} "更新成功" +// @Success 200 {object} Response{data=SwaggerRole} "更新成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" // @Failure 404 {object} Response "角色不存在" @@ -242,7 +242,7 @@ func (h *RoleHandler) UpdateRoleStatus(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "角色ID" -// @Success 200 {object} Response{data=[]domain.Permission} "权限列表" +// @Success 200 {object} Response{data=[]SwaggerPermission} "权限列表" // @Failure 404 {object} Response "角色不存在" // @Router /api/v1/roles/{id}/permissions [get] func (h *RoleHandler) GetRolePermissions(c *gin.Context) { @@ -278,7 +278,7 @@ func (h *RoleHandler) GetRolePermissions(c *gin.Context) { // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" // @Failure 404 {object} Response "角色不存在" -// @Router /api/v1/roles/{id}/permissions [post] +// @Router /api/v1/roles/{id}/permissions [put] func (h *RoleHandler) AssignPermissions(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { diff --git a/internal/api/handler/sms_handler.go b/internal/api/handler/sms_handler.go index ae954f7..13280c0 100644 --- a/internal/api/handler/sms_handler.go +++ b/internal/api/handler/sms_handler.go @@ -43,7 +43,7 @@ func NewSMSHandler(authService *service.AuthService, smsCodeService *service.SMS // @Success 200 {object} Response "发送成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 503 {object} Response "短信服务未配置" -// @Router /api/v1/sms/send [post] +// @Router /api/v1/auth/send-code [post] func (h *SMSHandler) SendCode(c *gin.Context) { if h.smsCodeService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS service not configured"}) @@ -80,7 +80,7 @@ func (h *SMSHandler) SendCode(c *gin.Context) { // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "验证码错误" // @Failure 503 {object} Response "短信登录未配置" -// @Router /api/v1/sms/login [post] +// @Router /api/v1/auth/login/code [post] func (h *SMSHandler) LoginByCode(c *gin.Context) { if h.authService == nil { c.JSON(http.StatusServiceUnavailable, gin.H{"code": 503, "message": "SMS login not configured"}) diff --git a/internal/api/handler/sso_handler.go b/internal/api/handler/sso_handler.go index a510628..342c165 100644 --- a/internal/api/handler/sso_handler.go +++ b/internal/api/handler/sso_handler.go @@ -3,6 +3,7 @@ package handler import ( "crypto/subtle" "net/http" + "strings" "time" "github.com/gin-gonic/gin" @@ -35,14 +36,14 @@ type AuthorizeRequest struct { // Authorize 处理 SSO 授权请求 // @Summary SSO 授权 -// @Description 处理 SSO 授权请求,返回授权码或访问令牌 +// @Description 处理 SSO 授权请求,返回授权码 // @Tags SSO // @Accept json // @Produce json // @Security BearerAuth // @Param client_id query string true "客户端ID" // @Param redirect_uri query string true "回调地址" -// @Param response_type query string true "响应类型" Enums(code, token) +// @Param response_type query string true "响应类型" Enums(code) // @Param scope query string false "授权范围" // @Param state query string false "状态参数" // @Success 302 {string} string "重定向到回调地址" @@ -57,21 +58,16 @@ func (h *SSOHandler) Authorize(c *gin.Context) { return } - // 验证 response_type - if req.ResponseType != "code" && req.ResponseType != "token" { + if req.ResponseType != "code" { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "unsupported response_type"}) return } - // 验证 redirect_uri 是否在白名单中 - if h.clientsStore != nil { - if !h.clientsStore.ValidateClientRedirectURI(req.ClientID, req.RedirectURI) { - c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid redirect_uri"}) - return - } + if h.clientsStore == nil || !h.clientsStore.ValidateClientRedirectURI(req.ClientID, req.RedirectURI) { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid redirect_uri"}) + return } - // 获取当前登录用户(从 auth middleware 设置的 context) userID, ok := getUserIDFromContext(c) if !ok { c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"}) @@ -84,60 +80,23 @@ func (h *SSOHandler) Authorize(c *gin.Context) { return } - // 生成授权码或 access token - if req.ResponseType == "code" { - code, err := h.ssoManager.GenerateAuthorizationCode( - req.ClientID, - req.RedirectURI, - req.Scope, - userID, - username, - ) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to generate code"}) - return - } - - // 重定向回客户端 - redirectURL := req.RedirectURI + "?code=" + code - if req.State != "" { - redirectURL += "&state=" + req.State - } - c.Redirect(http.StatusFound, redirectURL) - } else { - // implicit 模式,直接返回 token - code, err := h.ssoManager.GenerateAuthorizationCode( - req.ClientID, - req.RedirectURI, - req.Scope, - userID, - username, - ) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to generate code"}) - return - } - - // 验证授权码获取 session - session, err := h.ssoManager.ValidateAuthorizationCode(code) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to validate code"}) - return - } - - token, _, err := h.ssoManager.GenerateAccessToken(req.ClientID, session) - if err != nil { - c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to generate token"}) - return - } - - // 重定向回客户端,带 token - redirectURL := req.RedirectURI + "#access_token=" + token + "&expires_in=7200" - if req.State != "" { - redirectURL += "&state=" + req.State - } - c.Redirect(http.StatusFound, redirectURL) + code, err := h.ssoManager.GenerateAuthorizationCode( + req.ClientID, + req.RedirectURI, + req.Scope, + userID, + username, + ) + if err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to generate code"}) + return } + + redirectURL := req.RedirectURI + "?code=" + code + if req.State != "" { + redirectURL += "&state=" + req.State + } + c.Redirect(http.StatusFound, redirectURL) } // TokenRequest Token 请求 @@ -161,14 +120,14 @@ type TokenResponse struct { // @Summary 获取 Access Token // @Description 使用授权码获取 Access Token(授权码模式第二步) // @Tags SSO -// @Accept json +// @Accept x-www-form-urlencoded // @Produce json // @Param grant_type formData string true "授权类型" Enums(authorization_code) -// @Param code formData string false "授权码" -// @Param redirect_uri formData string false "回调地址" +// @Param code formData string true "授权码" +// @Param redirect_uri formData string true "回调地址" // @Param client_id formData string true "客户端ID" // @Param client_secret formData string true "客户端密钥" -// @Success 200 {object} TokenResponse "访问令牌响应" +// @Success 200 {object} Response{data=TokenResponse} "访问令牌响应" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "客户端认证失败" // @Failure 500 {object} Response "服务器错误" @@ -180,45 +139,50 @@ func (h *SSOHandler) Token(c *gin.Context) { return } - // 验证 grant_type if req.GrantType != "authorization_code" { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "unsupported grant_type"}) return } - - // 验证客户端凭证 - if h.clientsStore != nil { - client, err := h.clientsStore.GetByClientID(req.ClientID) - if err != nil { - c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid client"}) - return - } - // 使用常量时间比较防止时序攻击 - if subtle.ConstantTimeCompare([]byte(req.ClientSecret), []byte(client.ClientSecret)) != 1 { - c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid client_secret"}) - return - } + if req.Code == "" || req.RedirectURI == "" { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "code and redirect_uri are required"}) + return + } + + client, ok := h.authenticateClient(req.ClientID, req.ClientSecret) + if !ok { + c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid client credentials"}) + return + } + if !h.clientsStore.ValidateClientRedirectURI(client.ClientID, req.RedirectURI) { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid redirect_uri"}) + return } - // 验证授权码 session, err := h.ssoManager.ValidateAuthorizationCode(req.Code) if err != nil { c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid code"}) return } + if session.ClientID != req.ClientID || session.RedirectURI != req.RedirectURI { + c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "authorization code does not match client or redirect_uri"}) + return + } - // 生成 access token token, expiresAt, err := h.ssoManager.GenerateAccessToken(req.ClientID, session) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "failed to generate token"}) return } - c.JSON(http.StatusOK, TokenResponse{ - AccessToken: token, - TokenType: "Bearer", - ExpiresIn: int64(time.Until(expiresAt).Seconds()), - Scope: session.Scope, + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": TokenResponse{ + AccessToken: token, + TokenType: "Bearer", + ExpiresIn: int64(time.Until(expiresAt).Seconds()), + Scope: session.Scope, + }, }) } @@ -241,33 +205,46 @@ type IntrospectResponse struct { // @Summary 验证 Access Token // @Description 验证 Access Token 的有效性并返回相关信息 // @Tags SSO -// @Accept json +// @Accept x-www-form-urlencoded // @Produce json // @Param token formData string true "Access Token" -// @Param client_id formData string false "客户端ID" -// @Success 200 {object} IntrospectResponse "Token信息" +// @Param client_id formData string true "客户端ID" +// @Param client_secret formData string true "客户端密钥" +// @Success 200 {object} Response{data=IntrospectResponse} "Token信息" // @Failure 400 {object} Response "请求参数错误" -// @Failure 500 {object} Response "服务器错误" +// @Failure 401 {object} Response "客户端认证失败" // @Router /api/v1/sso/introspect [post] func (h *SSOHandler) Introspect(c *gin.Context) { - var req IntrospectRequest + var req struct { + Token string `form:"token" binding:"required"` + ClientID string `form:"client_id" binding:"required"` + ClientSecret string `form:"client_secret" binding:"required"` + } if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } + if _, ok := h.authenticateClient(req.ClientID, req.ClientSecret); !ok { + c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid client credentials"}) + return + } info, err := h.ssoManager.IntrospectToken(req.Token) if err != nil { - c.JSON(http.StatusOK, IntrospectResponse{Active: false}) + c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": IntrospectResponse{Active: false}}) return } - c.JSON(http.StatusOK, IntrospectResponse{ - Active: info.Active, - UserID: info.UserID, - Username: info.Username, - ExpiresAt: info.ExpiresAt.Unix(), - Scope: info.Scope, + c.JSON(http.StatusOK, gin.H{ + "code": 0, + "message": "success", + "data": IntrospectResponse{ + Active: info.Active, + UserID: info.UserID, + Username: info.Username, + ExpiresAt: info.ExpiresAt.Unix(), + Scope: info.Scope, + }, }) } @@ -280,22 +257,30 @@ type RevokeRequest struct { // @Summary 撤销 Access Token // @Description 撤销指定的 Access Token // @Tags SSO -// @Accept json +// @Accept x-www-form-urlencoded // @Produce json // @Param token formData string true "Access Token" +// @Param client_id formData string true "客户端ID" +// @Param client_secret formData string true "客户端密钥" // @Success 200 {object} Response "撤销成功" // @Failure 400 {object} Response "请求参数错误" -// @Failure 500 {object} Response "服务器错误" +// @Failure 401 {object} Response "客户端认证失败" // @Router /api/v1/sso/revoke [post] func (h *SSOHandler) Revoke(c *gin.Context) { - var req RevokeRequest + var req struct { + Token string `form:"token" binding:"required"` + ClientID string `form:"client_id" binding:"required"` + ClientSecret string `form:"client_secret" binding:"required"` + } if err := c.ShouldBind(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()}) return } - - h.ssoManager.RevokeToken(req.Token) - + if _, ok := h.authenticateClient(req.ClientID, req.ClientSecret); !ok { + c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid client credentials"}) + return + } + _ = h.ssoManager.RevokeToken(req.Token) c.JSON(http.StatusOK, gin.H{"code": 0, "message": "token revoked"}) } @@ -307,24 +292,23 @@ type UserInfoResponse struct { // UserInfo 获取当前用户信息 // @Summary 获取 SSO 用户信息 -// @Description 获取当前通过 SSO 授权的用户信息 +// @Description 获取当前通过 SSO Access Token 授权的用户信息 // @Tags SSO // @Produce json // @Security BearerAuth // @Success 200 {object} Response{data=UserInfoResponse} "用户信息" // @Failure 401 {object} Response "未认证" -// @Failure 500 {object} Response "服务器错误" // @Router /api/v1/sso/userinfo [get] func (h *SSOHandler) UserInfo(c *gin.Context) { - userID, ok := getUserIDFromContext(c) - if !ok { + token := extractBearerToken(c) + if token == "" { c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"}) return } - username, ok := getUsernameFromContext(c) - if !ok { - c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"}) + session, err := h.ssoManager.ValidateAccessToken(token) + if err != nil { + c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "invalid access token"}) return } @@ -332,8 +316,30 @@ func (h *SSOHandler) UserInfo(c *gin.Context) { "code": 0, "message": "success", "data": UserInfoResponse{ - UserID: userID, - Username: username, + UserID: session.UserID, + Username: session.Username, }, }) } + +func (h *SSOHandler) authenticateClient(clientID, clientSecret string) (*auth.SSOClient, bool) { + if h.clientsStore == nil { + return nil, false + } + client, err := h.clientsStore.GetByClientID(clientID) + if err != nil { + return nil, false + } + if subtle.ConstantTimeCompare([]byte(clientSecret), []byte(client.ClientSecret)) != 1 { + return nil, false + } + return client, true +} + +func extractBearerToken(c *gin.Context) string { + authorization := c.GetHeader("Authorization") + if !strings.HasPrefix(authorization, "Bearer ") { + return "" + } + return strings.TrimSpace(strings.TrimPrefix(authorization, "Bearer ")) +} diff --git a/internal/api/handler/sso_handler_test.go b/internal/api/handler/sso_handler_test.go index e0abc27..c28e1b5 100644 --- a/internal/api/handler/sso_handler_test.go +++ b/internal/api/handler/sso_handler_test.go @@ -1,347 +1,327 @@ package handler_test import ( + "bytes" + "encoding/json" "net/http" + "net/url" + "strings" "testing" - - "github.com/stretchr/testify/assert" ) -// ============================================================================= -// SSOHandler Tests - Single Sign-On -// ============================================================================= +type ssoWrappedResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data"` +} -// TestSSOHandler_Authorize_CodeFlow 验证授权码流程 -func TestSSOHandler_Authorize_CodeFlow(t *testing.T) { +type ssoTokenPayload struct { + AccessToken string `json:"access_token"` + TokenType string `json:"token_type"` + ExpiresIn int64 `json:"expires_in"` + Scope string `json:"scope"` +} + +type ssoIntrospectPayload struct { + Active bool `json:"active"` + UserID int64 `json:"user_id"` + Username string `json:"username"` + Scope string `json:"scope"` +} + +type ssoUserInfoPayload struct { + UserID int64 `json:"user_id"` + Username string `json:"username"` +} + +func doSSOAuthorizeRequest(t *testing.T, rawURL, bearer string) *http.Response { + t.Helper() + + req, err := http.NewRequest(http.MethodGet, rawURL, nil) + if err != nil { + t.Fatalf("build authorize request: %v", err) + } + if bearer != "" { + req.Header.Set("Authorization", "Bearer "+bearer) + } + + client := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }} + resp, err := client.Do(req) + if err != nil { + t.Fatalf("execute authorize request: %v", err) + } + return resp +} + +func doSSOFormPost(t *testing.T, rawURL string, form url.Values, bearer string) (*http.Response, []byte) { + t.Helper() + + req, err := http.NewRequest(http.MethodPost, rawURL, strings.NewReader(form.Encode())) + if err != nil { + t.Fatalf("build form request: %v", err) + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + if bearer != "" { + req.Header.Set("Authorization", "Bearer "+bearer) + } + + resp, err := (&http.Client{}).Do(req) + if err != nil { + t.Fatalf("execute form request: %v", err) + } + defer resp.Body.Close() + body := new(bytes.Buffer) + if _, err := body.ReadFrom(resp.Body); err != nil { + t.Fatalf("read form response: %v", err) + } + return resp, body.Bytes() +} + +func decodeSSOWrappedResponse(t *testing.T, body []byte) ssoWrappedResponse { + t.Helper() + + var wrapped ssoWrappedResponse + if err := json.Unmarshal(body, &wrapped); err != nil { + t.Fatalf("decode wrapped response failed: %v body=%s", err, string(body)) + } + return wrapped +} + +func extractAuthorizationCode(t *testing.T, location string) string { + t.Helper() + + parsed, err := url.Parse(location) + if err != nil { + t.Fatalf("parse redirect location failed: %v", err) + } + code := parsed.Query().Get("code") + if code == "" { + t.Fatalf("redirect location missing code: %s", location) + } + return code +} + +func issueSSOAuthCode(t *testing.T, serverURL, bearer string) string { + t.Helper() + + resp := doSSOAuthorizeRequest(t, serverURL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=code&scope=profile&state=abc", bearer) + defer resp.Body.Close() + if resp.StatusCode != http.StatusFound { + t.Fatalf("authorize expected 302, got %d", resp.StatusCode) + } + location := resp.Header.Get("Location") + if location == "" { + t.Fatal("authorize redirect missing Location header") + } + return extractAuthorizationCode(t, location) +} + +func exchangeSSOToken(t *testing.T, serverURL, code, redirectURI string) ssoTokenPayload { + t.Helper() + + resp, body := doSSOFormPost(t, serverURL+"/api/v1/sso/token", url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "client_id": {"test-client"}, + "client_secret": {"test-secret"}, + "redirect_uri": {redirectURI}, + }, "") + if resp.StatusCode != http.StatusOK { + t.Fatalf("token exchange expected 200, got %d body=%s", resp.StatusCode, string(body)) + } + + wrapped := decodeSSOWrappedResponse(t, body) + if wrapped.Code != 0 { + t.Fatalf("token exchange expected code=0, got %d body=%s", wrapped.Code, string(body)) + } + + var payload ssoTokenPayload + if err := json.Unmarshal(wrapped.Data, &payload); err != nil { + t.Fatalf("decode token payload failed: %v body=%s", err, string(body)) + } + if payload.AccessToken == "" { + t.Fatalf("token exchange returned empty access token: %s", string(body)) + } + return payload +} + +func TestSSOHandler_Authorize_CodeFlowRedirectsWithCodeAndState(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() - // Register and login user registerUser(server.URL, "ssouser", "sso@test.com", "Pass123!") - token := getToken(server.URL, "ssouser", "Pass123!") - assert.NotEmpty(t, token) + platformToken := getToken(server.URL, "ssouser", "Pass123!") + if platformToken == "" { + t.Fatal("expected login token for authorize flow") + } - // Request authorization with code flow - resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=code&state=xyz", token) + resp := doSSOAuthorizeRequest(t, server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=code&scope=profile&state=xyz", platformToken) defer resp.Body.Close() - // SSO may return various status codes based on configuration - assert.True(t, resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusBadRequest || - resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized, - "should handle authorize request, got %d", resp.StatusCode) + if resp.StatusCode != http.StatusFound { + t.Fatalf("authorize expected 302, got %d", resp.StatusCode) + } + location := resp.Header.Get("Location") + if location == "" { + t.Fatal("authorize redirect missing Location header") + } + if !strings.Contains(location, "code=") { + t.Fatalf("authorize redirect missing code: %s", location) + } + if !strings.Contains(location, "state=xyz") { + t.Fatalf("authorize redirect missing state: %s", location) + } } -// TestSSOHandler_Authorize_TokenFlow 验证隐式授权流程 -func TestSSOHandler_Authorize_TokenFlow(t *testing.T) { +func TestSSOHandler_Authorize_ImplicitFlowRejected(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() registerUser(server.URL, "ssouser2", "sso2@test.com", "Pass123!") - token := getToken(server.URL, "ssouser2", "Pass123!") - assert.NotEmpty(t, token) + platformToken := getToken(server.URL, "ssouser2", "Pass123!") + if platformToken == "" { + t.Fatal("expected login token for implicit rejection test") + } - // Request authorization with token flow - resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=token&state=abc", token) + resp := doSSOAuthorizeRequest(t, server.URL+"/api/v1/sso/authorize?client_id=test-client&redirect_uri=http://localhost/callback&response_type=token", platformToken) defer resp.Body.Close() - - assert.True(t, resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusBadRequest || - resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized, - "should handle token flow, got %d", resp.StatusCode) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("implicit flow expected 400, got %d", resp.StatusCode) + } } -// TestSSOHandler_Authorize_MissingParams 验证缺少参数 -func TestSSOHandler_Authorize_MissingParams(t *testing.T) { +func TestSSOHandler_Token_ExchangesWithoutPlatformBearerAuth(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() - registerUser(server.URL, "ssouser3", "sso3@test.com", "Pass123!") - token := getToken(server.URL, "ssouser3", "Pass123!") - - // Missing params - handler may enforce or not based on config - resp1, _ := doGet(server.URL+"/api/v1/sso/authorize?redirect_uri=http://localhost&response_type=code", token) - defer resp1.Body.Close() - assert.True(t, resp1.StatusCode >= http.StatusBadRequest || resp1.StatusCode == http.StatusOK, - "should handle missing client_id, got %d", resp1.StatusCode) - - // Missing redirect_uri - resp2, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&response_type=code", token) - defer resp2.Body.Close() - assert.True(t, resp2.StatusCode >= http.StatusBadRequest || resp2.StatusCode == http.StatusOK, - "should handle missing redirect_uri, got %d", resp2.StatusCode) - - // Missing response_type - resp3, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost", token) - defer resp3.Body.Close() - assert.True(t, resp3.StatusCode >= http.StatusBadRequest || resp3.StatusCode == http.StatusOK, - "should handle missing response_type, got %d", resp3.StatusCode) -} - -// TestSSOHandler_Authorize_InvalidResponseType 验证无效响应类型 -func TestSSOHandler_Authorize_InvalidResponseType(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - registerUser(server.URL, "ssouser4", "sso4@test.com", "Pass123!") - token := getToken(server.URL, "ssouser4", "Pass123!") - - resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=invalid", token) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK, - "should handle invalid response_type, got %d", resp.StatusCode) -} - -// TestSSOHandler_Authorize_Unauthorized 验证未认证用户 -func TestSSOHandler_Authorize_Unauthorized(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - // No authentication token - resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code", "") - defer resp.Body.Close() - - assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest, - "should handle unauthorized request, got %d", resp.StatusCode) -} - -// TestSSOHandler_Token_Success 验证获取 Token -func TestSSOHandler_Token_Success(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - // Try to exchange code for token using doPost helper - resp, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{ - "grant_type": "authorization_code", - "code": "invalid-code", - "client_id": "test", - "client_secret": "secret", - "redirect_uri": "http://localhost", - }) - defer resp.Body.Close() - - // Handler may accept or reject based on SSO config - assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK, - "should handle token request, got %d", resp.StatusCode) -} - -// TestSSOHandler_Token_MissingParams 验证缺少 Token 参数 -func TestSSOHandler_Token_MissingParams(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - // Missing client_id - resp1, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{ - "grant_type": "authorization_code", - "code": "test", - "client_secret": "secret", - }) - defer resp1.Body.Close() - assert.True(t, resp1.StatusCode >= http.StatusBadRequest || resp1.StatusCode == http.StatusOK, - "should handle missing client_id, got %d", resp1.StatusCode) - - // Missing client_secret - resp2, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{ - "grant_type": "authorization_code", - "code": "test", - "client_id": "test", - }) - defer resp2.Body.Close() - assert.True(t, resp2.StatusCode >= http.StatusBadRequest || resp2.StatusCode == http.StatusOK, - "should handle missing client_secret, got %d", resp2.StatusCode) - - // Missing grant_type - resp3, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{ - "client_id": "test", - "client_secret": "secret", - "code": "test", - }) - defer resp3.Body.Close() - assert.True(t, resp3.StatusCode >= http.StatusBadRequest || resp3.StatusCode == http.StatusOK, - "should handle missing grant_type, got %d", resp3.StatusCode) -} - -// TestSSOHandler_Token_InvalidGrantType 验证无效授权类型 -func TestSSOHandler_Token_InvalidGrantType(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - resp, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{ - "grant_type": "invalid_grant", - "client_id": "test", - "client_secret": "secret", - "code": "test", - }) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK, - "should handle invalid grant_type, got %d", resp.StatusCode) -} - -// TestSSOHandler_Introspect_Success 验证 Token 验证 -func TestSSOHandler_Introspect_Success(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - // Introspect invalid token - resp, body := doPost(server.URL+"/api/v1/sso/introspect", "", map[string]interface{}{ - "token": "invalid-token", - }) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest, - "should return introspect response, got %d: %s", resp.StatusCode, body) -} - -// TestSSOHandler_Introspect_MissingToken 验证缺少 Token -func TestSSOHandler_Introspect_MissingToken(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - resp, _ := doPost(server.URL+"/api/v1/sso/introspect", "", map[string]interface{}{ - "token": "", - }) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK, - "should handle missing token, got %d", resp.StatusCode) -} - -// TestSSOHandler_Revoke_Success 验证 Token 撤销 -func TestSSOHandler_Revoke_Success(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - resp, body := doPost(server.URL+"/api/v1/sso/revoke", "", map[string]interface{}{ - "token": "some-token", - }) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest, - "should handle revoke request, got %d: %s", resp.StatusCode, body) -} - -// TestSSOHandler_Revoke_MissingToken 验证缺少 Token -func TestSSOHandler_Revoke_MissingToken(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - resp, _ := doPost(server.URL+"/api/v1/sso/revoke", "", map[string]interface{}{ - "token": "", - }) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode >= http.StatusBadRequest || resp.StatusCode == http.StatusOK, - "should handle missing token, got %d", resp.StatusCode) -} - -// TestSSOHandler_UserInfo_Success 验证获取用户信息 -func TestSSOHandler_UserInfo_Success(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - registerUser(server.URL, "ssouser5", "sso5@test.com", "Pass123!") - token := getToken(server.URL, "ssouser5", "Pass123!") - assert.NotEmpty(t, token) - - resp, body := doGet(server.URL+"/api/v1/sso/userinfo", token) - defer resp.Body.Close() - - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden, - "should handle userinfo request, got %d: %s", resp.StatusCode, body) -} - -// TestSSOHandler_UserInfo_Unauthorized 验证未认证访问 -func TestSSOHandler_UserInfo_Unauthorized(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - resp, _ := doGet(server.URL+"/api/v1/sso/userinfo", "") - defer resp.Body.Close() - - assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusBadRequest, - "should handle unauthorized request, got %d", resp.StatusCode) -} - -// TestSSOHandler_FullFlow_Authorization 验证完整授权流程 -func TestSSOHandler_FullFlow_Authorization(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - // Register and login registerUser(server.URL, "flowuser", "flow@test.com", "Pass123!") - token := getToken(server.URL, "flowuser", "Pass123!") - assert.NotEmpty(t, token) + platformToken := getToken(server.URL, "flowuser", "Pass123!") + if platformToken == "" { + t.Fatal("expected login token for authorization") + } - // Step 1: Authorize (get code or redirect) - authResp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code&scope=profile", token) - defer authResp.Body.Close() + code := issueSSOAuthCode(t, server.URL, platformToken) + payload := exchangeSSOToken(t, server.URL, code, "http://localhost/callback") - // Step 2: Check response - SSO may return redirect or direct response based on config - assert.True(t, authResp.StatusCode == http.StatusFound || authResp.StatusCode == http.StatusOK || - authResp.StatusCode == http.StatusBadRequest || authResp.StatusCode == http.StatusInternalServerError, - "should handle authorization, got %d", authResp.StatusCode) - - if authResp.StatusCode == http.StatusFound { - location := authResp.Header.Get("Location") - assert.Contains(t, location, "localhost") - t.Logf("Redirected to: %s", location) + if payload.TokenType != "Bearer" { + t.Fatalf("unexpected token type: %q", payload.TokenType) + } + if payload.ExpiresIn <= 0 { + t.Fatalf("unexpected expires_in: %d", payload.ExpiresIn) } } -// TestSSOHandler_ClientCredentials_Validation 验证客户端凭证验证 -func TestSSOHandler_ClientCredentials_Validation(t *testing.T) { +func TestSSOHandler_Token_RedirectURIMismatchRejected(t *testing.T) { server, cleanup := setupHandlerTestServer(t) defer cleanup() - // Try with invalid client credentials - resp, _ := doPost(server.URL+"/api/v1/sso/token", "", map[string]interface{}{ - "grant_type": "authorization_code", - "code": "test-code", - "client_id": "invalid-client", - "client_secret": "wrong-secret", - "redirect_uri": "http://localhost", - }) - defer resp.Body.Close() + registerUser(server.URL, "mismatchuser", "mismatch@test.com", "Pass123!") + platformToken := getToken(server.URL, "mismatchuser", "Pass123!") + if platformToken == "" { + t.Fatal("expected login token for authorization") + } - // May accept or reject based on SSO configuration - assert.True(t, resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK, - "should handle client credentials, got %d", resp.StatusCode) -} - -// TestSSOHandler_Scope_Handling 验证 Scope 处理 -func TestSSOHandler_Scope_Handling(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - registerUser(server.URL, "scopeuser", "scope@test.com", "Pass123!") - token := getToken(server.URL, "scopeuser", "Pass123!") - - // Request with scope - resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code&scope=profile+email", token) - defer resp.Body.Close() - - // Should handle scope parameter - assert.True(t, resp.StatusCode == http.StatusFound || resp.StatusCode == http.StatusBadRequest || - resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusUnauthorized, - "should handle scope parameter, got %d", resp.StatusCode) -} - -// TestSSOHandler_State_Preservation 验证 State 参数保持 -func TestSSOHandler_State_Preservation(t *testing.T) { - server, cleanup := setupHandlerTestServer(t) - defer cleanup() - - registerUser(server.URL, "stateuser", "state@test.com", "Pass123!") - token := getToken(server.URL, "stateuser", "Pass123!") - - // Request with state parameter - resp, _ := doGet(server.URL+"/api/v1/sso/authorize?client_id=test&redirect_uri=http://localhost&response_type=code&state=my-state-value", token) - defer resp.Body.Close() - - // If redirected, state should be preserved in callback - if resp.StatusCode == http.StatusFound { - location := resp.Header.Get("Location") - // State should be included in redirect URL - t.Logf("Redirect location: %s", location) + code := issueSSOAuthCode(t, server.URL, platformToken) + resp, body := doSSOFormPost(t, server.URL+"/api/v1/sso/token", url.Values{ + "grant_type": {"authorization_code"}, + "code": {code}, + "client_id": {"test-client"}, + "client_secret": {"test-secret"}, + "redirect_uri": {"http://localhost/other"}, + }, "") + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("redirect mismatch expected 400, got %d body=%s", resp.StatusCode, string(body)) + } +} + +func TestSSOHandler_IntrospectAndRevokeUseClientCredentialsNotPlatformBearer(t *testing.T) { + server, cleanup := setupHandlerTestServer(t) + defer cleanup() + + registerUser(server.URL, "introspectuser", "introspect@test.com", "Pass123!") + platformToken := getToken(server.URL, "introspectuser", "Pass123!") + if platformToken == "" { + t.Fatal("expected login token for authorization") + } + + code := issueSSOAuthCode(t, server.URL, platformToken) + tokenPayload := exchangeSSOToken(t, server.URL, code, "http://localhost/callback") + + resp1, body1 := doSSOFormPost(t, server.URL+"/api/v1/sso/introspect", url.Values{ + "token": {tokenPayload.AccessToken}, + "client_id": {"test-client"}, + "client_secret": {"test-secret"}, + }, "") + if resp1.StatusCode != http.StatusOK { + t.Fatalf("introspect expected 200, got %d body=%s", resp1.StatusCode, string(body1)) + } + wrapped1 := decodeSSOWrappedResponse(t, body1) + var introspect ssoIntrospectPayload + if err := json.Unmarshal(wrapped1.Data, &introspect); err != nil { + t.Fatalf("decode introspect payload failed: %v body=%s", err, string(body1)) + } + if !introspect.Active { + t.Fatalf("expected active token in introspect response: %s", string(body1)) + } + if introspect.Username != "introspectuser" { + t.Fatalf("unexpected introspect username: %q", introspect.Username) + } + + resp2, body2 := doSSOFormPost(t, server.URL+"/api/v1/sso/revoke", url.Values{ + "token": {tokenPayload.AccessToken}, + "client_id": {"test-client"}, + "client_secret": {"test-secret"}, + }, "") + if resp2.StatusCode != http.StatusOK { + t.Fatalf("revoke expected 200, got %d body=%s", resp2.StatusCode, string(body2)) + } + + resp3, body3 := doSSOFormPost(t, server.URL+"/api/v1/sso/introspect", url.Values{ + "token": {tokenPayload.AccessToken}, + "client_id": {"test-client"}, + "client_secret": {"test-secret"}, + }, "") + if resp3.StatusCode != http.StatusOK { + t.Fatalf("post-revoke introspect expected 200, got %d body=%s", resp3.StatusCode, string(body3)) + } + wrapped3 := decodeSSOWrappedResponse(t, body3) + var revoked ssoIntrospectPayload + if err := json.Unmarshal(wrapped3.Data, &revoked); err != nil { + t.Fatalf("decode revoked introspect payload failed: %v body=%s", err, string(body3)) + } + if revoked.Active { + t.Fatalf("expected revoked token to be inactive: %s", string(body3)) + } +} + +func TestSSOHandler_UserInfoUsesSSOAccessTokenSubject(t *testing.T) { + server, cleanup := setupHandlerTestServer(t) + defer cleanup() + + registerUser(server.URL, "userinfo-user", "userinfo@test.com", "Pass123!") + platformToken := getToken(server.URL, "userinfo-user", "Pass123!") + if platformToken == "" { + t.Fatal("expected login token for authorization") + } + + code := issueSSOAuthCode(t, server.URL, platformToken) + tokenPayload := exchangeSSOToken(t, server.URL, code, "http://localhost/callback") + + resp, body := doGet(server.URL+"/api/v1/sso/userinfo", tokenPayload.AccessToken) + if resp.StatusCode != http.StatusOK { + t.Fatalf("userinfo expected 200, got %d body=%s", resp.StatusCode, body) + } + wrapped := decodeSSOWrappedResponse(t, []byte(body)) + var payload ssoUserInfoPayload + if err := json.Unmarshal(wrapped.Data, &payload); err != nil { + t.Fatalf("decode userinfo payload failed: %v body=%s", err, body) + } + if payload.Username != "userinfo-user" { + t.Fatalf("unexpected userinfo username: %q body=%s", payload.Username, body) + } + if payload.UserID == 0 { + t.Fatalf("userinfo user_id should be non-zero: %s", body) } } diff --git a/internal/api/handler/swagger_domain_aliases.go b/internal/api/handler/swagger_domain_aliases.go new file mode 100644 index 0000000..abeebd1 --- /dev/null +++ b/internal/api/handler/swagger_domain_aliases.go @@ -0,0 +1 @@ +package handler diff --git a/internal/api/handler/swagger_domain_types.go b/internal/api/handler/swagger_domain_types.go new file mode 100644 index 0000000..5a3a5e0 --- /dev/null +++ b/internal/api/handler/swagger_domain_types.go @@ -0,0 +1,83 @@ +package handler + +import "time" + +type SwaggerRole struct { + ID int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Description string `json:"description"` + Status int `json:"status"` + IsSystem bool `json:"is_system"` + Sort int `json:"sort"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type SwaggerPermission struct { + ID int64 `json:"id"` + Name string `json:"name"` + Code string `json:"code"` + Description string `json:"description"` + Type int `json:"type"` + ParentID *int64 `json:"parent_id,omitempty"` + Path string `json:"path"` + Method string `json:"method"` + Icon string `json:"icon"` + Sort int `json:"sort"` + Status int `json:"status"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type SwaggerCustomField struct { + ID int64 `json:"id"` + Name string `json:"name"` + FieldKey string `json:"field_key"` + FieldType string `json:"field_type"` + Required bool `json:"required"` + SortOrder int `json:"sort_order"` + Options string `json:"options,omitempty"` + Placeholder string `json:"placeholder,omitempty"` + HelpText string `json:"help_text,omitempty"` + Active bool `json:"active"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type SwaggerDevice struct { + ID int64 `json:"id"` + UserID int64 `json:"user_id"` + DeviceID string `json:"device_id"` + DeviceName string `json:"device_name"` + DeviceType int `json:"device_type"` + DeviceOS string `json:"device_os"` + DeviceBrowser string `json:"device_browser"` + IP string `json:"ip"` + Location string `json:"location"` + Status int `json:"status"` + LastActiveAt *time.Time `json:"last_active_at,omitempty"` + IsTrusted bool `json:"is_trusted"` + TrustedUntil *time.Time `json:"trusted_until,omitempty"` + LastUsedAt *time.Time `json:"last_used_at,omitempty"` + Current bool `json:"current"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type SwaggerTheme struct { + ID int64 `json:"id"` + Name string `json:"name"` + IsDefault bool `json:"is_default"` + PrimaryColor string `json:"primary_color"` + SecondaryColor string `json:"secondary_color"` + AccentColor string `json:"accent_color"` + BackgroundColor string `json:"background_color"` + TextColor string `json:"text_color"` + SuccessColor string `json:"success_color"` + WarningColor string `json:"warning_color"` + ErrorColor string `json:"error_color"` + InfoColor string `json:"info_color"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} diff --git a/internal/api/handler/swagger_request_types.go b/internal/api/handler/swagger_request_types.go new file mode 100644 index 0000000..bcb54e4 --- /dev/null +++ b/internal/api/handler/swagger_request_types.go @@ -0,0 +1,138 @@ +package handler + +// TOTPVerifyRequest documents the password-login TOTP verification request. +type TOTPVerifyRequest struct { + UserID int64 `json:"user_id"` + Code string `json:"code"` + DeviceID string `json:"device_id,omitempty"` + TempToken string `json:"temp_token"` +} + +// RefreshTokenRequest documents refresh token input. +type RefreshTokenRequest struct { + RefreshToken string `json:"refresh_token"` +} + +// ResendActivationRequest documents resend activation input. +type ResendActivationRequest struct { + Email string `json:"email"` +} + +// SendEmailCodeRequest documents email code login input. +type SendEmailCodeRequest struct { + Email string `json:"email"` +} + +// LoginByEmailCodeRequest documents email-code login input. +type LoginByEmailCodeRequest struct { + Email string `json:"email"` + Code string `json:"code"` + DeviceID string `json:"device_id,omitempty"` + DeviceName string `json:"device_name,omitempty"` + DeviceBrowser string `json:"device_browser,omitempty"` + DeviceOS string `json:"device_os,omitempty"` +} + +// BootstrapAdminRequest documents bootstrap admin input. +type BootstrapAdminRequest struct { + Username string `json:"username"` + Email string `json:"email"` + Password string `json:"password"` +} + +// VerifyCaptchaRequest documents captcha verification input. +type VerifyCaptchaRequest struct { + CaptchaID string `json:"captcha_id"` + Answer string `json:"answer"` +} + +// ForgotPasswordRequest documents email-based password reset initiation. +type ForgotPasswordRequest struct { + Email string `json:"email"` +} + +// ResetPasswordRequest documents token-based password reset input. +type ResetPasswordRequest struct { + Token string `json:"token"` + NewPassword string `json:"new_password"` +} + +// EnableTOTPRequest documents enabling TOTP with a code. +type EnableTOTPRequest struct { + Code string `json:"code"` +} + +// DisableTOTPRequest documents disabling TOTP with a code. +type DisableTOTPRequest struct { + Code string `json:"code"` +} + +// VerifyTOTPRequest documents authenticated TOTP verification input. +type VerifyTOTPRequest struct { + Code string `json:"code"` + DeviceID string `json:"device_id,omitempty"` +} + +// CreateUserRequest documents user creation input. +type CreateUserRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email,omitempty"` + Phone string `json:"phone,omitempty"` + Nickname string `json:"nickname,omitempty"` +} + +// UpdateUserRequest documents user profile updates. +type UpdateUserRequest struct { + Email *string `json:"email,omitempty"` + Nickname *string `json:"nickname,omitempty"` +} + +// UpdatePasswordRequest documents password change input. +type UpdatePasswordRequest struct { + OldPassword string `json:"old_password"` + NewPassword string `json:"new_password"` +} + +// UpdateStatusRequest documents status updates for users. +type UpdateStatusRequest struct { + Status string `json:"status"` +} + +// AssignRolesRequest documents role assignment input. +type AssignRolesRequest struct { + RoleIDs []int64 `json:"role_ids"` +} + +// CreateAdminRequest documents admin creation input. +type CreateAdminRequest struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email,omitempty"` + Nickname string `json:"nickname,omitempty"` +} + +// SetUserFieldValuesRequest documents user custom-field updates. +type SetUserFieldValuesRequest struct { + Values map[string]string `json:"values"` +} + +// UpdateDeviceStatusRequest documents device status changes. +type UpdateDeviceStatusRequest struct { + Status string `json:"status"` +} + +// UpdatePermissionStatusRequest documents permission status changes. +type UpdatePermissionStatusRequest struct { + Status string `json:"status"` +} + +// UpdateRoleStatusRequest documents role status changes. +type UpdateRoleStatusRequest struct { + Status string `json:"status"` +} + +// AssignPermissionsRequest documents role permission assignment. +type AssignPermissionsRequest struct { + PermissionIDs []int64 `json:"permission_ids"` +} diff --git a/internal/api/handler/swagger_types.go b/internal/api/handler/swagger_types.go new file mode 100644 index 0000000..3bcea9a --- /dev/null +++ b/internal/api/handler/swagger_types.go @@ -0,0 +1,115 @@ +package handler + +// Response is the canonical API envelope used in Swagger annotations. +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// CaptchaResponse is the captcha generation payload. +type CaptchaResponse struct { + CaptchaID string `json:"captcha_id"` + Image string `json:"image"` +} + +// VerifyResponse represents a boolean verification result. +type VerifyResponse struct { + Verified bool `json:"verified"` +} + +// ValidateTokenResponse represents password reset token validation output. +type ValidateTokenResponse struct { + Valid bool `json:"valid"` +} + +// TOTPStatusResponse represents whether TOTP is enabled. +type TOTPStatusResponse struct { + Enabled bool `json:"enabled"` +} + +// TOTPSetupResponse contains setup material for enabling TOTP. +type TOTPSetupResponse struct { + Secret string `json:"secret"` + QRCodeBase64 string `json:"qr_code_base64"` + RecoveryCodes []string `json:"recovery_codes"` +} + +// VerifyTOTPResponse represents a successful TOTP verification. +type VerifyTOTPResponse struct { + Verified bool `json:"verified"` +} + +// DeviceListResponse represents paginated device results. +type DeviceListResponse struct { + Items interface{} `json:"items"` + Total int64 `json:"total"` + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` + Cursor string `json:"cursor,omitempty"` + NextCursor string `json:"next_cursor,omitempty"` + HasMore bool `json:"has_more,omitempty"` +} + +// LoginLogListResponse represents paginated login log results. +type LoginLogListResponse struct { + List interface{} `json:"list,omitempty"` + Items interface{} `json:"items,omitempty"` + Total int64 `json:"total"` + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` + Cursor string `json:"cursor,omitempty"` + NextCursor string `json:"next_cursor,omitempty"` + HasMore bool `json:"has_more,omitempty"` +} + +// OperationLogListResponse represents paginated operation log results. +type OperationLogListResponse struct { + List interface{} `json:"list,omitempty"` + Items interface{} `json:"items,omitempty"` + Total int64 `json:"total"` + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` + Cursor string `json:"cursor,omitempty"` + NextCursor string `json:"next_cursor,omitempty"` + HasMore bool `json:"has_more,omitempty"` +} + +// RoleListResponse represents paginated role results. +type RoleListResponse struct { + Items interface{} `json:"items"` + Total int64 `json:"total"` + Page int `json:"page,omitempty"` + PageSize int `json:"page_size,omitempty"` +} + +// UserListResponse represents list or cursor user results. +type UserListResponse struct { + Users interface{} `json:"users,omitempty"` + Items interface{} `json:"items,omitempty"` + Total int64 `json:"total,omitempty"` + Offset int `json:"offset,omitempty"` + Limit int `json:"limit,omitempty"` + NextCursor string `json:"next_cursor,omitempty"` + HasMore bool `json:"has_more,omitempty"` + PageSize int `json:"page_size,omitempty"` +} + +// AvatarResponse represents the avatar upload result. +type AvatarResponse struct { + AvatarURL string `json:"avatar_url"` + Thumbnail string `json:"thumbnail"` +} + +// CSRFTokenResponse documents the empty CSRF compatibility payload. +type CSRFTokenResponse struct { + Token string `json:"token"` +} + +// OAuthProvidersResponse documents enabled OAuth providers. +type OAuthProvidersResponse struct { + Providers []string `json:"providers"` +} + +// CustomFieldValuesResponse documents arbitrary custom-field values. +type CustomFieldValuesResponse map[string]string diff --git a/internal/api/handler/theme_handler.go b/internal/api/handler/theme_handler.go index ac95ff6..7848104 100644 --- a/internal/api/handler/theme_handler.go +++ b/internal/api/handler/theme_handler.go @@ -27,7 +27,7 @@ func NewThemeHandler(themeService *service.ThemeService) *ThemeHandler { // @Produce json // @Security BearerAuth // @Param request body service.CreateThemeRequest true "主题信息" -// @Success 201 {object} Response{data=domain.Theme} "主题创建成功" +// @Success 201 {object} Response{data=SwaggerTheme} "主题创建成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" @@ -61,7 +61,7 @@ func (h *ThemeHandler) CreateTheme(c *gin.Context) { // @Security BearerAuth // @Param id path int true "主题ID" // @Param request body service.UpdateThemeRequest true "更新信息" -// @Success 200 {object} Response{data=domain.Theme} "主题更新成功" +// @Success 200 {object} Response{data=SwaggerTheme} "主题更新成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" @@ -129,7 +129,7 @@ func (h *ThemeHandler) DeleteTheme(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "主题ID" -// @Success 200 {object} Response{data=domain.Theme} "主题详情" +// @Success 200 {object} Response{data=SwaggerTheme} "主题详情" // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" @@ -160,7 +160,7 @@ func (h *ThemeHandler) GetTheme(c *gin.Context) { // @Tags 主题管理 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=[]domain.Theme} "主题列表" +// @Success 200 {object} Response{data=[]SwaggerTheme} "主题列表" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" // @Router /api/v1/themes [get] @@ -184,10 +184,10 @@ func (h *ThemeHandler) ListThemes(c *gin.Context) { // @Tags 主题管理 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=[]domain.Theme} "主题列表" +// @Success 200 {object} Response{data=[]SwaggerTheme} "主题列表" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/themes/all [get] +// @Router /api/v1/themes [get] func (h *ThemeHandler) ListAllThemes(c *gin.Context) { themes, err := h.themeService.ListAllThemes(c.Request.Context()) if err != nil { @@ -208,7 +208,7 @@ func (h *ThemeHandler) ListAllThemes(c *gin.Context) { // @Tags 主题管理 // @Produce json // @Security BearerAuth -// @Success 200 {object} Response{data=domain.Theme} "默认主题" +// @Success 200 {object} Response{data=SwaggerTheme} "默认主题" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" // @Router /api/v1/themes/default [get] @@ -237,7 +237,7 @@ func (h *ThemeHandler) GetDefaultTheme(c *gin.Context) { // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/themes/{id}/default [put] +// @Router /api/v1/themes/default/{id} [put] func (h *ThemeHandler) SetDefaultTheme(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { @@ -261,9 +261,9 @@ func (h *ThemeHandler) SetDefaultTheme(c *gin.Context) { // @Description 获取当前系统正在使用的主题(公开接口) // @Tags 主题管理 // @Produce json -// @Success 200 {object} Response{data=domain.Theme} "当前生效主题" +// @Success 200 {object} Response{data=SwaggerTheme} "当前生效主题" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/themes/active [get] +// @Router /api/v1/theme/active [get] func (h *ThemeHandler) GetActiveTheme(c *gin.Context) { theme, err := h.themeService.GetActiveTheme(c.Request.Context()) if err != nil { diff --git a/internal/api/handler/theme_handler_test.go b/internal/api/handler/theme_handler_test.go index b631e0c..909f8bb 100644 --- a/internal/api/handler/theme_handler_test.go +++ b/internal/api/handler/theme_handler_test.go @@ -24,7 +24,7 @@ func TestThemeHandler_ListThemes_Success(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/themes", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || + assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError, "should list themes, got %d: %s", resp.StatusCode, body) } @@ -42,7 +42,7 @@ func TestThemeHandler_ListAllThemes_Success(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/themes/all", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || + assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusBadRequest, "should list all themes, got %d: %s", resp.StatusCode, body) } @@ -60,7 +60,7 @@ func TestThemeHandler_GetTheme_Success(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/themes/1", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || + assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusInternalServerError, "should get theme, got %d: %s", resp.StatusCode, body) } @@ -94,7 +94,7 @@ func TestThemeHandler_GetTheme_InvalidID(t *testing.T) { resp, _ := doGet(server.URL+"/api/v1/themes/invalid", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusForbidden, "should handle invalid ID, got %d", resp.StatusCode) } @@ -112,7 +112,7 @@ func TestThemeHandler_GetDefaultTheme_Success(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/themes/default", token) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || + assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusInternalServerError, "should get default theme, got %d: %s", resp.StatusCode, body) } @@ -126,7 +126,7 @@ func TestThemeHandler_GetActiveTheme_Success(t *testing.T) { resp, body := doGet(server.URL+"/api/v1/themes/active", "") defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || + assert.True(t, resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError || resp.StatusCode == http.StatusUnauthorized, "should get active theme, got %d: %s", resp.StatusCode, body) } @@ -142,9 +142,9 @@ func TestThemeHandler_CreateTheme_Success(t *testing.T) { } resp, body := doPost(server.URL+"/api/v1/themes", token, map[string]interface{}{ - "name": "dark-theme", + "name": "dark-theme", "display_name": "Dark Theme", - "description": "A dark theme for the application", + "description": "A dark theme for the application", "colors": map[string]string{ "primary": "#1a1a1a", "secondary": "#2d2d2d", @@ -185,7 +185,7 @@ func TestThemeHandler_CreateTheme_NonAdmin(t *testing.T) { assert.NotEmpty(t, token) resp, _ := doPost(server.URL+"/api/v1/themes", token, map[string]interface{}{ - "name": "test-theme", + "name": "test-theme", "display_name": "Test Theme", }) defer resp.Body.Close() @@ -248,7 +248,7 @@ func TestThemeHandler_UpdateTheme_InvalidID(t *testing.T) { }) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError, "should handle invalid ID, got %d", resp.StatusCode) } @@ -350,7 +350,7 @@ func TestThemeHandler_SetDefaultTheme_InvalidID(t *testing.T) { resp, _ := doPut(server.URL+"/api/v1/themes/invalid/default", token, nil) defer resp.Body.Close() - assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNotFound || resp.StatusCode == http.StatusInternalServerError, "should handle invalid ID, got %d", resp.StatusCode) } @@ -384,7 +384,7 @@ func TestThemeHandler_CRUD_FullFlow(t *testing.T) { // List themes resp1, _ := doGet(server.URL+"/api/v1/themes", token) defer resp1.Body.Close() - assert.True(t, resp1.StatusCode == http.StatusOK || resp1.StatusCode == http.StatusForbidden || + assert.True(t, resp1.StatusCode == http.StatusOK || resp1.StatusCode == http.StatusForbidden || resp1.StatusCode == http.StatusInternalServerError || resp1.StatusCode == http.StatusBadRequest, "should list themes, got %d", resp1.StatusCode) diff --git a/internal/api/handler/totp_handler.go b/internal/api/handler/totp_handler.go index 70443f8..de3d2d4 100644 --- a/internal/api/handler/totp_handler.go +++ b/internal/api/handler/totp_handler.go @@ -30,7 +30,7 @@ func NewTOTPHandler(authService *service.AuthService, totpService *service.TOTPS // @Security BearerAuth // @Success 200 {object} Response{data=TOTPStatusResponse} "TOTP状态" // @Failure 401 {object} Response "未认证" -// @Router /api/v1/auth/totp/status [get] +// @Router /api/v1/auth/2fa/status [get] func (h *TOTPHandler) GetTOTPStatus(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -57,7 +57,7 @@ func (h *TOTPHandler) GetTOTPStatus(c *gin.Context) { // @Success 200 {object} Response{data=TOTPSetupResponse} "TOTP设置信息" // @Failure 401 {object} Response "未认证" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/auth/totp/setup [post] +// @Router /api/v1/auth/2fa/setup [get] func (h *TOTPHandler) SetupTOTP(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -94,7 +94,7 @@ func (h *TOTPHandler) SetupTOTP(c *gin.Context) { // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证或验证码错误" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/auth/totp/enable [post] +// @Router /api/v1/auth/2fa/enable [post] func (h *TOTPHandler) EnableTOTP(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -131,7 +131,7 @@ func (h *TOTPHandler) EnableTOTP(c *gin.Context) { // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证或验证码错误" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/auth/totp/disable [post] +// @Router /api/v1/auth/2fa/disable [post] func (h *TOTPHandler) DisableTOTP(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { @@ -168,7 +168,7 @@ func (h *TOTPHandler) DisableTOTP(c *gin.Context) { // @Failure 400 {object} Response "请求参数错误" // @Failure 401 {object} Response "未认证或验证码错误" // @Failure 500 {object} Response "服务器错误" -// @Router /api/v1/auth/totp/verify [post] +// @Router /api/v1/auth/2fa/verify [post] func (h *TOTPHandler) VerifyTOTP(c *gin.Context) { userID, ok := getUserIDFromContext(c) if !ok { diff --git a/internal/api/handler/totp_handler_test.go b/internal/api/handler/totp_handler_test.go index f3330c1..5a95ded 100644 --- a/internal/api/handler/totp_handler_test.go +++ b/internal/api/handler/totp_handler_test.go @@ -141,7 +141,7 @@ func TestTOTPHandler_EnableTOTP_InvalidCode(t *testing.T) { defer resp.Body.Close() // Should reject invalid code (could be 400, 401, or 500 depending on implementation) - assert.True(t, resp.StatusCode == http.StatusBadRequest || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusInternalServerError, "should reject invalid code, got %d", resp.StatusCode) @@ -195,7 +195,7 @@ func TestTOTPHandler_EnableTOTP_AlreadyEnabled(t *testing.T) { defer resp2.Body.Close() // Could succeed, fail with bad request, or internal error - assert.True(t, resp2.StatusCode == http.StatusBadRequest || + assert.True(t, resp2.StatusCode == http.StatusBadRequest || resp2.StatusCode == http.StatusOK || resp2.StatusCode == http.StatusInternalServerError, "should return appropriate status, got %d", resp2.StatusCode) @@ -291,7 +291,7 @@ func TestTOTPHandler_VerifyTOTP_NotEnabled(t *testing.T) { defer resp.Body.Close() // Should fail since 2FA not enabled (could be 400 or 500) - assert.True(t, resp.StatusCode == http.StatusBadRequest || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusInternalServerError, "should error when 2FA not enabled, got %d", resp.StatusCode) @@ -315,7 +315,7 @@ func TestTOTPHandler_VerifyTOTP_InvalidCode(t *testing.T) { defer resp.Body.Close() // Should fail since 2FA not enabled or code invalid - assert.True(t, resp.StatusCode == http.StatusBadRequest || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusInternalServerError, "should reject, got %d", resp.StatusCode) @@ -354,7 +354,7 @@ func TestTOTPHandler_VerifyTOTP_WithDeviceID(t *testing.T) { defer resp.Body.Close() // Should fail for various reasons but accept the request format - assert.True(t, resp.StatusCode == http.StatusBadRequest || + assert.True(t, resp.StatusCode == http.StatusBadRequest || resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusInternalServerError, "should process request but fail validation, got %d", resp.StatusCode) @@ -394,7 +394,7 @@ func TestTOTPHandler_FullFlow_SetupEnableDisable(t *testing.T) { "code": "000000", }) defer resp3.Body.Close() - assert.True(t, resp3.StatusCode == http.StatusBadRequest || + assert.True(t, resp3.StatusCode == http.StatusBadRequest || resp3.StatusCode == http.StatusUnauthorized || resp3.StatusCode == http.StatusInternalServerError, "should fail with invalid code, got %d", resp3.StatusCode) diff --git a/internal/api/handler/user_handler.go b/internal/api/handler/user_handler.go index 6215e5a..5c66adc 100644 --- a/internal/api/handler/user_handler.go +++ b/internal/api/handler/user_handler.go @@ -355,7 +355,7 @@ func (h *UserHandler) UpdateUserStatus(c *gin.Context) { // @Produce json // @Security BearerAuth // @Param id path int true "用户ID" -// @Success 200 {object} Response{data=[]domain.Role} "角色列表" +// @Success 200 {object} Response{data=[]SwaggerRole} "角色列表" // @Failure 403 {object} Response "无权限" // @Failure 404 {object} Response "用户不存在" // @Router /api/v1/users/{id}/roles [get] @@ -399,7 +399,7 @@ func (h *UserHandler) GetUserRoles(c *gin.Context) { // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" // @Failure 404 {object} Response "用户不存在" -// @Router /api/v1/users/{id}/roles [post] +// @Router /api/v1/users/{id}/roles [put] func (h *UserHandler) AssignRoles(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { @@ -488,7 +488,7 @@ func (h *UserHandler) BatchDelete(c *gin.Context) { // @Security BearerAuth // @Success 200 {object} Response{data=[]UserResponse} "管理员列表" // @Failure 403 {object} Response "无权限" -// @Router /api/v1/users/admins [get] +// @Router /api/v1/admin/admins [get] func (h *UserHandler) ListAdmins(c *gin.Context) { admins, err := h.userService.ListAdmins(c.Request.Context()) if err != nil { @@ -515,7 +515,7 @@ func (h *UserHandler) ListAdmins(c *gin.Context) { // @Success 201 {object} Response{data=UserResponse} "管理员创建成功" // @Failure 400 {object} Response "请求参数错误" // @Failure 403 {object} Response "无权限" -// @Router /api/v1/users/admins [post] +// @Router /api/v1/admin/admins [post] func (h *UserHandler) CreateAdmin(c *gin.Context) { var req struct { Username string `json:"username" binding:"required"` @@ -556,7 +556,7 @@ func (h *UserHandler) CreateAdmin(c *gin.Context) { // @Failure 400 {object} Response "无效的用户ID" // @Failure 403 {object} Response "无权限" // @Failure 409 {object} Response "无法删除(最后管理员或自删)" -// @Router /api/v1/users/admins/{id} [delete] +// @Router /api/v1/admin/admins/{id} [delete] func (h *UserHandler) DeleteAdmin(c *gin.Context) { id, err := strconv.ParseInt(c.Param("id"), 10, 64) if err != nil { diff --git a/internal/api/handler/user_handler_test.go b/internal/api/handler/user_handler_test.go index b4b9d43..bea5c57 100644 --- a/internal/api/handler/user_handler_test.go +++ b/internal/api/handler/user_handler_test.go @@ -316,7 +316,7 @@ func TestUserHandler_UpdatePassword_Success(t *testing.T) { "new_password": "NewPass456!", }) defer resp.Body.Close() - + // Accept both 200 (success) and 403 (if user doesn't have permission to update self) // The handler checks: currentUserID != id && !IsAdmin(c) // For self-update, currentUserID == id, so should be allowed @@ -589,7 +589,7 @@ func TestUserHandler_BatchDelete_Success(t *testing.T) { } // Batch delete uses DELETE method with body - req, _ := http.NewRequest("DELETE", server.URL+"/api/v1/users/batch", + req, _ := http.NewRequest("DELETE", server.URL+"/api/v1/users/batch", bytes.NewReader([]byte(`{"ids": [2, 3, 4]}`))) req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") @@ -599,7 +599,7 @@ func TestUserHandler_BatchDelete_Success(t *testing.T) { t.Fatalf("request failed: %v", err) } defer resp.Body.Close() - + // Accept 200 or method not allowed if resp.StatusCode == http.StatusOK { bodyBytes, _ := io.ReadAll(resp.Body) diff --git a/internal/api/router/router.go b/internal/api/router/router.go index ec736d8..8a253e1 100644 --- a/internal/api/router/router.go +++ b/internal/api/router/router.go @@ -381,15 +381,19 @@ func (r *Router) Setup() *gin.Engine { } } - // SSO 单点登录接口(需要认证) + // SSO 单点登录接口 if r.ssoHandler != nil { - sso := protected.Group("/sso") + ssoProtected := protected.Group("/sso") { - sso.GET("/authorize", r.ssoHandler.Authorize) - sso.POST("/token", r.ssoHandler.Token) - sso.POST("/introspect", r.ssoHandler.Introspect) - sso.POST("/revoke", r.ssoHandler.Revoke) - sso.GET("/userinfo", r.ssoHandler.UserInfo) + ssoProtected.GET("/authorize", r.ssoHandler.Authorize) + } + + ssoPublic := v1.Group("/sso") + { + ssoPublic.POST("/token", r.ssoHandler.Token) + ssoPublic.POST("/introspect", r.ssoHandler.Introspect) + ssoPublic.POST("/revoke", r.ssoHandler.Revoke) + ssoPublic.GET("/userinfo", r.ssoHandler.UserInfo) } } } diff --git a/internal/auth/sso.go b/internal/auth/sso.go index 7380044..28c1a0b 100644 --- a/internal/auth/sso.go +++ b/internal/auth/sso.go @@ -61,17 +61,18 @@ type SSOTokenInfo struct { ClientID string } -// SSOSession SSO Session type SSOSession struct { - SessionID string - UserID int64 - Username string - ClientID string - CreatedAt time.Time - ExpiresAt time.Time - Scope string + SessionID string + UserID int64 + Username string + ClientID string + RedirectURI string + CreatedAt time.Time + ExpiresAt time.Time + Scope string } + // SSOManager SSO 管理器 type SSOManager struct { mu sync.RWMutex @@ -113,20 +114,19 @@ func (m *SSOManager) GenerateAuthorizationCode(clientID, redirectURI, scope stri } session := &SSOSession{ - SessionID: sessionID, - UserID: userID, - Username: username, - ClientID: clientID, - CreatedAt: time.Now(), - ExpiresAt: time.Now().Add(10 * time.Minute), // 授权码 10 分钟有效期 - Scope: scope, + SessionID: sessionID, + UserID: userID, + Username: username, + ClientID: clientID, + RedirectURI: redirectURI, + CreatedAt: time.Now(), + ExpiresAt: time.Now().Add(10 * time.Minute), + Scope: scope, } m.mu.Lock() - // 检查并清理过期 session,如果超过限制则淘汰最旧的 if len(m.sessions) >= MaxSessions { m.cleanupExpiredLocked() - // 如果仍然满,淘汰最早的 if len(m.sessions) >= MaxSessions { m.evictOldest() } @@ -137,6 +137,7 @@ func (m *SSOManager) GenerateAuthorizationCode(clientID, redirectURI, scope stri return code, nil } + // ValidateAuthorizationCode 验证授权码 func (m *SSOManager) ValidateAuthorizationCode(code string) (*SSOSession, error) { m.mu.Lock() @@ -167,13 +168,14 @@ func (m *SSOManager) GenerateAccessToken(clientID string, session *SSOSession) ( expiresAt := time.Now().Add(2 * time.Hour) // Access token 2 小时有效期 accessSession := &SSOSession{ - SessionID: token, - UserID: session.UserID, - Username: session.Username, - ClientID: clientID, - CreatedAt: time.Now(), - ExpiresAt: expiresAt, - Scope: session.Scope, + SessionID: token, + UserID: session.UserID, + Username: session.Username, + ClientID: clientID, + RedirectURI: session.RedirectURI, + CreatedAt: time.Now(), + ExpiresAt: expiresAt, + Scope: session.Scope, } m.mu.Lock() @@ -225,6 +227,27 @@ func (m *SSOManager) RevokeToken(token string) error { delete(m.sessions, token) return nil } +func (m *SSOManager) ValidateAccessToken(token string) (*SSOSession, error) { + m.mu.RLock() + session, ok := m.sessions[token] + if !ok { + m.mu.RUnlock() + return nil, errors.New("invalid access token") + } + if time.Now().After(session.ExpiresAt) { + m.mu.RUnlock() + m.mu.Lock() + delete(m.sessions, token) + m.mu.Unlock() + return nil, errors.New("access token expired") + } + if session.RedirectURI == "" { + m.mu.RUnlock() + return nil, errors.New("not an access token") + } + m.mu.RUnlock() + return session, nil +} // CleanupExpired 清理过期的 session func (m *SSOManager) CleanupExpired() { diff --git a/internal/config/config.go b/internal/config/config.go index 4a427eb..71e4872 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1133,7 +1133,7 @@ func load(allowMissingJWTSecret bool) (*Config, error) { } if cfg.JWT.Secret != "" && isWeakJWTSecret(cfg.JWT.Secret) { - slog.Warn("JWT secret appears weak; use a 32+ character random secret in production.") + return nil, fmt.Errorf("validate config error: jwt.secret is too weak") } if len(cfg.Security.ResponseHeaders.AdditionalAllowed) > 0 || len(cfg.Security.ResponseHeaders.ForceRemove) > 0 { slog.Info("response header policy configured", @@ -1546,6 +1546,9 @@ func (c *Config) Validate() error { if jwtSecret == "" { return fmt.Errorf("jwt.secret is required") } + if isWeakJWTSecret(jwtSecret) { + return fmt.Errorf("jwt.secret is too weak") + } // NOTE: 按 UTF-8 编码后的字节长度计算。 // 选择 bytes 而不是 rune 计数,确保二进制/随机串的长度语义更接近“熵”而非“字符数”。 if len([]byte(jwtSecret)) < 32 { diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 7ee9f53..309bc1d 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -17,7 +17,25 @@ func resetViperWithJWTSecret(t *testing.T) { t.Setenv("JWT_SECRET", strings.Repeat("x", 32)) } -func TestLoadForBootstrapAllowsMissingJWTSecret(t *testing.T) { +func TestLoadRejectsWeakJWTSecret(t *testing.T) { + viper.Reset() + t.Setenv("JWT_SECRET", strings.Repeat("x", 32)) + + configPath := filepath.Join(t.TempDir(), "weak-jwt.yaml") + require.NoError(t, os.WriteFile(configPath, []byte("jwt:\n secret: changeme\n"), 0o644)) + t.Setenv("CONFIG_FILE", configPath) + t.Setenv("JWT_SECRET", "") + + _, err := Load() + if err == nil { + t.Fatal("Load() should reject weak jwt secret") + } + if !strings.Contains(err.Error(), "jwt.secret is too weak") { + t.Fatalf("Load() error = %v, want weak-secret failure", err) + } +} + +func TestLoadForBootstrapStillAllowsMissingJWTSecret(t *testing.T) { viper.Reset() t.Setenv("JWT_SECRET", "") diff --git a/internal/e2e/e2e_test.go b/internal/e2e/e2e_test.go index d54d74d..99e69b0 100644 --- a/internal/e2e/e2e_test.go +++ b/internal/e2e/e2e_test.go @@ -132,6 +132,12 @@ func setupRealServer(t *testing.T) (*httptest.Server, func()) { avatarH := handler.NewAvatarHandler(userRepo) ssoManager := auth.NewSSOManager() ssoClientsStore := auth.NewDefaultSSOClientsStore() + ssoClientsStore.RegisterClient(&auth.SSOClient{ + ClientID: "test-client", + ClientSecret: "test-secret", + Name: "E2E Test Client", + RedirectURIs: []string{"http://localhost/callback"}, + }) ssoH := handler.NewSSOHandler(ssoManager, ssoClientsStore) rateLimitMW := middleware.NewRateLimitMiddleware(config.RateLimitConfig{}) diff --git a/internal/service/auth_cache_invalidator.go b/internal/service/auth_cache_invalidator.go new file mode 100644 index 0000000..36d3339 --- /dev/null +++ b/internal/service/auth_cache_invalidator.go @@ -0,0 +1,48 @@ +package service + +import ( + "context" + "sort" +) + +// AuthCacheInvalidator invalidates auth-related caches after security-sensitive writes. +type AuthCacheInvalidator interface { + InvalidateUserState(userID int64) + InvalidateUserPerms(userID int64) +} + +type nopAuthCacheInvalidator struct{} + +func (nopAuthCacheInvalidator) InvalidateUserState(int64) {} +func (nopAuthCacheInvalidator) InvalidateUserPerms(int64) {} + +func normalizeAuthCacheInvalidator(invalidator AuthCacheInvalidator) AuthCacheInvalidator { + if invalidator == nil { + return nopAuthCacheInvalidator{} + } + return invalidator +} + +func collectSortedUniqueUserIDs(ctx context.Context, idsFunc func(context.Context) ([]int64, error), fallback func(context.Context) ([]int64, error)) ([]int64, error) { + ids, err := idsFunc(ctx) + if err != nil && fallback != nil { + ids, err = fallback(ctx) + } + if err != nil { + return nil, err + } + if len(ids) == 0 { + return []int64{}, nil + } + seen := make(map[int64]struct{}, len(ids)) + unique := make([]int64, 0, len(ids)) + for _, id := range ids { + if _, ok := seen[id]; ok { + continue + } + seen[id] = struct{}{} + unique = append(unique, id) + } + sort.Slice(unique, func(i, j int) bool { return unique[i] < unique[j] }) + return unique, nil +} diff --git a/internal/service/auth_cache_invalidator_test.go b/internal/service/auth_cache_invalidator_test.go new file mode 100644 index 0000000..1eb2f74 --- /dev/null +++ b/internal/service/auth_cache_invalidator_test.go @@ -0,0 +1,167 @@ +package service_test + +import ( + "context" + "strconv" + "testing" + "time" + + "github.com/user-management-system/internal/cache" + "github.com/user-management-system/internal/domain" + "github.com/user-management-system/internal/repository" + "github.com/user-management-system/internal/service" + gormsqlite "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +type cacheInvalidatorHarness struct { + l1 *cache.L1Cache +} + +func (h *cacheInvalidatorHarness) InvalidateUserState(userID int64) { + h.l1.Delete("user_state:" + strconv.FormatInt(userID, 10)) +} + +func (h *cacheInvalidatorHarness) InvalidateUserPerms(userID int64) { + h.l1.Delete("user_perms:" + strconv.FormatInt(userID, 10)) +} + +func setupCacheInvalidationDB(t *testing.T) *gorm.DB { + t.Helper() + db, err := gorm.Open(gormsqlite.New(gormsqlite.Config{ + DriverName: "sqlite", + DSN: "file:cache_invalidation?mode=memory&cache=shared", + }), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)}) + if err != nil { + t.Fatalf("open sqlite failed: %v", err) + } + if err := db.AutoMigrate(&domain.User{}, &domain.Role{}, &domain.UserRole{}, &domain.Permission{}, &domain.RolePermission{}); err != nil { + t.Fatalf("migrate failed: %v", err) + } + return db +} + +func TestUserService_InvalidateStateCacheOnStatusChange(t *testing.T) { + db := setupCacheInvalidationDB(t) + userRepo := repository.NewUserRepository(db) + userRoleRepo := repository.NewUserRoleRepository(db) + roleRepo := repository.NewRoleRepository(db) + userSvc := service.NewUserService(userRepo, userRoleRepo, roleRepo, nil) + l1 := cache.NewL1Cache() + userSvc.SetAuthCacheInvalidator(&cacheInvalidatorHarness{l1: l1}) + + user := &domain.User{Username: "statecache", Password: "x", Status: domain.UserStatusActive} + if err := db.Create(user).Error; err != nil { + t.Fatalf("create user failed: %v", err) + } + l1.Set("user_state:"+strconv.FormatInt(user.ID, 10), "cached", time.Minute) + + if err := userSvc.UpdateStatus(context.Background(), user.ID, domain.UserStatusInactive); err != nil { + t.Fatalf("UpdateStatus failed: %v", err) + } + if _, ok := l1.Get("user_state:" + strconv.FormatInt(user.ID, 10)); ok { + t.Fatal("expected user_state cache to be invalidated") + } +} + +func TestUserService_InvalidatePermCacheOnAssignRoles(t *testing.T) { + db := setupCacheInvalidationDB(t) + userRepo := repository.NewUserRepository(db) + userRoleRepo := repository.NewUserRoleRepository(db) + roleRepo := repository.NewRoleRepository(db) + userSvc := service.NewUserService(userRepo, userRoleRepo, roleRepo, nil) + l1 := cache.NewL1Cache() + userSvc.SetAuthCacheInvalidator(&cacheInvalidatorHarness{l1: l1}) + + user := &domain.User{Username: "permcache", Password: "x", Status: domain.UserStatusActive} + role := &domain.Role{Name: "role1", Code: "role1", Status: domain.RoleStatusEnabled} + if err := db.Create(user).Error; err != nil { + t.Fatalf("create user failed: %v", err) + } + if err := db.Create(role).Error; err != nil { + t.Fatalf("create role failed: %v", err) + } + l1.Set("user_perms:"+strconv.FormatInt(user.ID, 10), "cached", time.Minute) + + if err := userSvc.AssignRoles(context.Background(), user.ID, []int64{role.ID}); err != nil { + t.Fatalf("AssignRoles failed: %v", err) + } + if _, ok := l1.Get("user_perms:" + strconv.FormatInt(user.ID, 10)); ok { + t.Fatal("expected user_perms cache to be invalidated") + } +} + +func TestRoleService_InvalidatePermCacheOnAssignPermissions(t *testing.T) { + db := setupCacheInvalidationDB(t) + roleRepo := repository.NewRoleRepository(db) + rolePermRepo := repository.NewRolePermissionRepository(db) + userRoleRepo := repository.NewUserRoleRepository(db) + roleSvc := service.NewRoleService(roleRepo, rolePermRepo) + roleSvc.SetUserRoleRepository(userRoleRepo) + l1 := cache.NewL1Cache() + roleSvc.SetAuthCacheInvalidator(&cacheInvalidatorHarness{l1: l1}) + + user := &domain.User{Username: "rolepermcache", Password: "x", Status: domain.UserStatusActive} + role := &domain.Role{Name: "role2", Code: "role2", Status: domain.RoleStatusEnabled} + perm := &domain.Permission{Name: "perm1", Code: "perm1", Type: domain.PermissionTypeMenu, Status: domain.PermissionStatusEnabled} + if err := db.Create(user).Error; err != nil { + t.Fatalf("create user failed: %v", err) + } + if err := db.Create(role).Error; err != nil { + t.Fatalf("create role failed: %v", err) + } + if err := db.Create(perm).Error; err != nil { + t.Fatalf("create permission failed: %v", err) + } + if err := db.Create(&domain.UserRole{UserID: user.ID, RoleID: role.ID}).Error; err != nil { + t.Fatalf("create user role failed: %v", err) + } + l1.Set("user_perms:"+strconv.FormatInt(user.ID, 10), "cached", time.Minute) + + if err := roleSvc.AssignPermissions(context.Background(), role.ID, []int64{perm.ID}); err != nil { + t.Fatalf("AssignPermissions failed: %v", err) + } + if _, ok := l1.Get("user_perms:" + strconv.FormatInt(user.ID, 10)); ok { + t.Fatal("expected user_perms cache to be invalidated after role permission change") + } +} + +func TestPermissionService_InvalidatePermCacheOnStatusChange(t *testing.T) { + db := setupCacheInvalidationDB(t) + permRepo := repository.NewPermissionRepository(db) + rolePermRepo := repository.NewRolePermissionRepository(db) + userRoleRepo := repository.NewUserRoleRepository(db) + permSvc := service.NewPermissionService(permRepo) + permSvc.SetRolePermissionRepository(rolePermRepo) + permSvc.SetUserRoleRepository(userRoleRepo) + l1 := cache.NewL1Cache() + permSvc.SetAuthCacheInvalidator(&cacheInvalidatorHarness{l1: l1}) + + user := &domain.User{Username: "permstatuscache", Password: "x", Status: domain.UserStatusActive} + role := &domain.Role{Name: "role3", Code: "role3", Status: domain.RoleStatusEnabled} + perm := &domain.Permission{Name: "perm2", Code: "perm2", Type: domain.PermissionTypeMenu, Status: domain.PermissionStatusEnabled} + if err := db.Create(user).Error; err != nil { + t.Fatalf("create user failed: %v", err) + } + if err := db.Create(role).Error; err != nil { + t.Fatalf("create role failed: %v", err) + } + if err := db.Create(perm).Error; err != nil { + t.Fatalf("create permission failed: %v", err) + } + if err := db.Create(&domain.UserRole{UserID: user.ID, RoleID: role.ID}).Error; err != nil { + t.Fatalf("create user role failed: %v", err) + } + if err := db.Create(&domain.RolePermission{RoleID: role.ID, PermissionID: perm.ID}).Error; err != nil { + t.Fatalf("create role permission failed: %v", err) + } + l1.Set("user_perms:"+strconv.FormatInt(user.ID, 10), "cached", time.Minute) + + if err := permSvc.UpdatePermissionStatus(context.Background(), perm.ID, domain.PermissionStatusDisabled); err != nil { + t.Fatalf("UpdatePermissionStatus failed: %v", err) + } + if _, ok := l1.Get("user_perms:" + strconv.FormatInt(user.ID, 10)); ok { + t.Fatal("expected user_perms cache to be invalidated after permission status change") + } +} diff --git a/internal/service/business_logic_test.go b/internal/service/business_logic_test.go index eb15036..c570cc7 100644 --- a/internal/service/business_logic_test.go +++ b/internal/service/business_logic_test.go @@ -180,6 +180,12 @@ func setupTestEnv(t *testing.T) *testEnv { avatarH := handler.NewAvatarHandler(userRepo) ssoManager := auth.NewSSOManager() ssoClientsStore := auth.NewDefaultSSOClientsStore() + ssoClientsStore.RegisterClient(&auth.SSOClient{ + ClientID: "test-client", + ClientSecret: "test-secret", + Name: "Service Test Client", + RedirectURIs: []string{"http://localhost/callback"}, + }) ssoH := handler.NewSSOHandler(ssoManager, ssoClientsStore) _ = permSvc // suppress unused warning diff --git a/internal/service/permission.go b/internal/service/permission.go index f7445bf..e96ab45 100644 --- a/internal/service/permission.go +++ b/internal/service/permission.go @@ -8,9 +8,11 @@ import ( "github.com/user-management-system/internal/repository" ) -// PermissionService 权限服务 type PermissionService struct { - permissionRepo *repository.PermissionRepository + permissionRepo *repository.PermissionRepository + rolePermissionRepo *repository.RolePermissionRepository + userRoleRepo *repository.UserRoleRepository + authCacheInvalidator AuthCacheInvalidator } // NewPermissionService 创建权限服务 @@ -18,10 +20,23 @@ func NewPermissionService( permissionRepo *repository.PermissionRepository, ) *PermissionService { return &PermissionService{ - permissionRepo: permissionRepo, + permissionRepo: permissionRepo, + authCacheInvalidator: normalizeAuthCacheInvalidator(nil), } } +func (s *PermissionService) SetRolePermissionRepository(rolePermissionRepo *repository.RolePermissionRepository) { + s.rolePermissionRepo = rolePermissionRepo +} + +func (s *PermissionService) SetUserRoleRepository(userRoleRepo *repository.UserRoleRepository) { + s.userRoleRepo = userRoleRepo +} + +func (s *PermissionService) SetAuthCacheInvalidator(invalidator AuthCacheInvalidator) { + s.authCacheInvalidator = normalizeAuthCacheInvalidator(invalidator) +} + // CreatePermissionRequest 创建权限请求 type CreatePermissionRequest struct { Name string `json:"name" binding:"required"` @@ -195,7 +210,25 @@ func (s *PermissionService) ListPermissions(ctx context.Context, req *ListPermis // UpdatePermissionStatus 更新权限状态 func (s *PermissionService) UpdatePermissionStatus(ctx context.Context, permissionID int64, status domain.PermissionStatus) error { - return s.permissionRepo.UpdateStatus(ctx, permissionID, status) + if err := s.permissionRepo.UpdateStatus(ctx, permissionID, status); err != nil { + return err + } + if s.rolePermissionRepo != nil && s.userRoleRepo != nil { + roleIDs, err := s.rolePermissionRepo.GetRoleIDByPermissionID(ctx, permissionID) + if err != nil { + return err + } + for _, roleID := range roleIDs { + userIDs, err := s.userRoleRepo.GetUserIDByRoleID(ctx, roleID) + if err != nil { + return err + } + for _, userID := range userIDs { + s.authCacheInvalidator.InvalidateUserPerms(userID) + } + } + } + return nil } // GetPermissionTree 获取权限树 diff --git a/internal/service/role.go b/internal/service/role.go index f0fd77f..1ab1334 100644 --- a/internal/service/role.go +++ b/internal/service/role.go @@ -10,10 +10,11 @@ import ( "github.com/user-management-system/internal/repository" ) -// RoleService 角色服务 type RoleService struct { - roleRepo *repository.RoleRepository - rolePermissionRepo *repository.RolePermissionRepository + roleRepo *repository.RoleRepository + rolePermissionRepo *repository.RolePermissionRepository + userRoleRepo *repository.UserRoleRepository + authCacheInvalidator AuthCacheInvalidator } // NewRoleService 创建角色服务 @@ -22,11 +23,20 @@ func NewRoleService( rolePermissionRepo *repository.RolePermissionRepository, ) *RoleService { return &RoleService{ - roleRepo: roleRepo, - rolePermissionRepo: rolePermissionRepo, + roleRepo: roleRepo, + rolePermissionRepo: rolePermissionRepo, + authCacheInvalidator: normalizeAuthCacheInvalidator(nil), } } +func (s *RoleService) SetUserRoleRepository(userRoleRepo *repository.UserRoleRepository) { + s.userRoleRepo = userRoleRepo +} + +func (s *RoleService) SetAuthCacheInvalidator(invalidator AuthCacheInvalidator) { + s.authCacheInvalidator = normalizeAuthCacheInvalidator(invalidator) +} + // CreateRoleRequest 创建角色请求 type CreateRoleRequest struct { Name string `json:"name" binding:"required"` @@ -225,13 +235,22 @@ func (s *RoleService) UpdateRoleStatus(ctx context.Context, roleID int64, status if err != nil { return errors.New("角色不存在") } - - // 系统角色不能禁用 if role.IsSystem && status == domain.RoleStatusDisabled { return errors.New("系统角色不能禁用") } - - return s.roleRepo.UpdateStatus(ctx, roleID, status) + if err := s.roleRepo.UpdateStatus(ctx, roleID, status); err != nil { + return err + } + if s.userRoleRepo != nil { + userIDs, err := s.userRoleRepo.GetUserIDByRoleID(ctx, roleID) + if err != nil { + return err + } + for _, userID := range userIDs { + s.authCacheInvalidator.InvalidateUserPerms(userID) + } + } + return nil } // GetRolePermissions 获取角色权限(包含继承的父角色权限) @@ -261,12 +280,10 @@ func (s *RoleService) GetRolePermissions(ctx context.Context, roleID int64) ([]* // AssignPermissions 分配权限 func (s *RoleService) AssignPermissions(ctx context.Context, roleID int64, permissionIDs []int64) error { - // 删除原有权限 if err := s.rolePermissionRepo.DeleteByRoleID(ctx, roleID); err != nil { return err } - // 创建新权限关联 var rolePermissions []*domain.RolePermission for _, permissionID := range permissionIDs { rolePermissions = append(rolePermissions, &domain.RolePermission{ @@ -275,5 +292,17 @@ func (s *RoleService) AssignPermissions(ctx context.Context, roleID int64, permi }) } - return s.rolePermissionRepo.BatchCreate(ctx, rolePermissions) + if err := s.rolePermissionRepo.BatchCreate(ctx, rolePermissions); err != nil { + return err + } + if s.userRoleRepo != nil { + userIDs, err := s.userRoleRepo.GetUserIDByRoleID(ctx, roleID) + if err != nil { + return err + } + for _, userID := range userIDs { + s.authCacheInvalidator.InvalidateUserPerms(userID) + } + } + return nil } diff --git a/internal/service/user_service.go b/internal/service/user_service.go index 5153f40..38e9194 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -55,12 +55,12 @@ type passwordHistoryRepository interface { DeleteOldRecords(ctx context.Context, userID int64, keep int) error } -// UserService 用户服务 type UserService struct { - userRepo userRepository - userRoleRepo userRoleRepository - roleRepo roleRepository - passwordHistoryRepo passwordHistoryRepository + userRepo userRepository + userRoleRepo userRoleRepository + roleRepo roleRepository + passwordHistoryRepo passwordHistoryRepository + authCacheInvalidator AuthCacheInvalidator } const passwordHistoryLimit = 5 // 保留最近5条密码历史 @@ -73,13 +73,18 @@ func NewUserService( passwordHistoryRepo passwordHistoryRepository, ) *UserService { return &UserService{ - userRepo: userRepo, - userRoleRepo: userRoleRepo, - roleRepo: roleRepo, - passwordHistoryRepo: passwordHistoryRepo, + userRepo: userRepo, + userRoleRepo: userRoleRepo, + roleRepo: roleRepo, + passwordHistoryRepo: passwordHistoryRepo, + authCacheInvalidator: normalizeAuthCacheInvalidator(nil), } } +func (s *UserService) SetAuthCacheInvalidator(invalidator AuthCacheInvalidator) { + s.authCacheInvalidator = normalizeAuthCacheInvalidator(invalidator) +} + // ChangePassword 修改用户密码(含历史记录检查) func (s *UserService) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error { if s.userRepo == nil { @@ -131,10 +136,14 @@ func (s *UserService) ChangePassword(ctx context.Context, userID int64, oldPassw user.PasswordChangedAt = time.Now() if s.passwordHistoryRepo == nil { - return s.userRepo.Update(ctx, user) + if err := s.userRepo.Update(ctx, user); err != nil { + return err + } + s.authCacheInvalidator.InvalidateUserState(userID) + return nil } - return s.userRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { + if err := s.userRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := tx.Model(&domain.User{}). Where("id = ?", user.ID). Updates(map[string]interface{}{"password": user.Password, "password_changed_at": user.PasswordChangedAt}).Error; err != nil { @@ -167,7 +176,13 @@ func (s *UserService) ChangePassword(ctx context.Context, userID int64, oldPassw } } return nil - }) + }); err != nil { + return err + } + + s.authCacheInvalidator.InvalidateUserState(userID) + return nil + } // GetByID 根据ID获取用户 @@ -317,7 +332,11 @@ func (s *UserService) ListCursor(ctx context.Context, req *ListCursorRequest) (* // UpdateStatus 更新用户状态 func (s *UserService) UpdateStatus(ctx context.Context, id int64, status domain.UserStatus) error { - return s.userRepo.UpdateStatus(ctx, id, status) + if err := s.userRepo.UpdateStatus(ctx, id, status); err != nil { + return err + } + s.authCacheInvalidator.InvalidateUserState(id) + return nil } // BatchUpdateStatusRequest 批量更新状态请求 @@ -334,6 +353,11 @@ type BatchDeleteRequest struct { // BatchUpdateStatus 批量更新用户状态 func (s *UserService) BatchUpdateStatus(ctx context.Context, req *BatchUpdateStatusRequest) (int64, error) { err := s.userRepo.BatchUpdateStatus(ctx, req.IDs, req.Status) + if err == nil { + for _, id := range req.IDs { + s.authCacheInvalidator.InvalidateUserState(id) + } + } return int64(len(req.IDs)), err } @@ -377,20 +401,21 @@ func (s *UserService) GetUserRoles(ctx context.Context, userID int64) ([]*domain // AssignRoles 分配用户角色 func (s *UserService) AssignRoles(ctx context.Context, userID int64, roleIDs []int64) error { - // 检查用户是否存在 if _, err := s.userRepo.GetByID(ctx, userID); err != nil { return err } - // 验证所有角色存在(预先验证,避免在事务内做不必要的查询) for _, roleID := range roleIDs { if _, err := s.roleRepo.GetByID(ctx, roleID); err != nil { return fmt.Errorf("角色 %d 不存在", roleID) } } - // 使用 Repository 层的事务方法替换用户角色(原子操作) - return s.userRoleRepo.ReplaceUserRoles(ctx, userID, roleIDs) + if err := s.userRoleRepo.ReplaceUserRoles(ctx, userID, roleIDs); err != nil { + return err + } + s.authCacheInvalidator.InvalidateUserPerms(userID) + return nil } // getAdminRoleID looks up the admin role ID by code to avoid hardcoded magic numbers. @@ -485,17 +510,13 @@ func (s *UserService) CreateAdmin(ctx context.Context, req *CreateAdminRequest) // DeleteAdmin 删除管理员(移除管理员角色) func (s *UserService) DeleteAdmin(ctx context.Context, userID int64, currentUserID int64) error { - // 检查用户是否存在 if _, err := s.userRepo.GetByID(ctx, userID); err != nil { return err } - - // 不能删除自己 if currentUserID == userID { return errors.New("不能删除自己") } - // 检查是否是最后一个管理员(保护) adminRoleID, err := s.getAdminRoleID(ctx) if err != nil { return err @@ -508,8 +529,11 @@ func (s *UserService) DeleteAdmin(ctx context.Context, userID int64, currentUser return errors.New("不能删除最后一个管理员") } - // 删除用户的管理员角色 - return s.userRoleRepo.DeleteByUserAndRole(ctx, userID, adminRoleID) + if err := s.userRoleRepo.DeleteByUserAndRole(ctx, userID, adminRoleID); err != nil { + return err + } + s.authCacheInvalidator.InvalidateUserPerms(userID) + return nil } // CreateAdminRequest 创建管理员请求