補齊平台帳號(platform_native)的密碼自助能力,並讓未完成 Email 驗證的使用者可恢復註冊;OIDC/LDAP/SCIM 帳號禁止在本系統變更密碼。登入若已啟用 TOTP 改為兩階段驗證,OTP 重送加入 60 秒冷卻;同步調整 golangci 排除路徑與 zitadel lint 修正。 Co-authored-by: Cursor <cursoragent@cursor.com> |
||
|---|---|---|
| .. | ||
| README.md | ||
| auth.api | ||
| common.api | ||
| gateway.api | ||
| member.api | ||
| normal.api | ||
| permission.api | ||
README.md
API 定義(goctl + go-doc 共用)
檔案
| 檔案 | 用途 |
|---|---|
gateway.api |
入口:info() + import |
common.api |
共用文件型別(APIErrorStatus、ErrorDetail) |
auth.api |
Auth 路由(scope 28) |
member.api |
Member 路由(scope 29) |
permission.api |
Permission / RBAC 路由(scope 31) |
normal.api |
路由與業務 data 型別 |
指令
make gen-api # 生成 handler / logic / types
make gen-doc # 生成 docs/openapi/gateway.yaml(OpenAPI 3.0)
註解約定
- Logic
returns:只寫業務 data(如PingData) - 文件
@respdoc:寫實際 HTTP JSON(如PingOKStatus、APIErrorStatus) @doc:單一 API 的 summary / description- 多狀態碼用
/* @respdoc-200 ... */區塊,放在@handler前 - Request 驗證:欄位可加
validate:"required,email"等 tag;make gen-api後 handler 會自動ValidateAll(見generate/goctl/api/handler.tpl)
文件分組與欄位說明(go-doc 規則)
OpenAPI 由 generate/doc-generate(go-doc)從 .api 直接讀取,下列三種寫法必須遵守,AI agent 在新增 / 修改 API 時請一併照做:
1. Swagger UI 分組(tags)
每個 .api 檔的 @server (...) 區塊必須帶 tags: 與 summary:,否則 endpoint 會散落在 "default" 群組。
@server (
group: permission // goctl 用:決定 handler 子目錄
prefix: /api/v1/permissions
tags: "Permission - 權限" // ← go-doc 用:Swagger UI 分組標題
summary: "Catalog / 角色 / 使用者角色 / 外部映射 / Policy"
)
一個
.api內可以開多個@server區塊(相同group/prefix但不同tags:),把同一模組再拆成子組。
2. 欄位中文 description
go-doc 把欄位的 description 只認 backtick 結尾的同一行 // 註解,寫在前一行不會被解析。
// ❌ 不會被當 description
RegisterReq {
// 租戶 slug
TenantSlug string `json:"tenant_slug" validate:"required"`
}
// ✅ 正確寫法(backtick 行末 //)
RegisterReq {
TenantSlug string `json:"tenant_slug" validate:"required"` // 租戶 slug(小寫英數)
Email string `json:"email" validate:"required,email"` // 電子郵件
Password string `json:"password" validate:"required,min=8,max=128"` // 密碼(8-128 字元)
}
所有 Request 型別(命名 *Req / *Query / *Path 的 struct)的每個欄位都要加中文 description。Response / Data 型別可選。
3. Enum 列舉值(Swagger UI 下拉選單)
validate:"oneof=A B C" 是 runtime 驗證用的,go-doc 不會解析。要讓 Swagger UI 顯示下拉選單,必須在 tag 內加 options=A|B|C(管道分隔):
// ❌ 只有 validate 不會出 enum
Provider string `json:"provider" validate:"required,oneof=google"`
// ✅ 同時保留兩者:options= 給 go-doc,validate= 給 runtime
Provider string `json:"provider,options=google" validate:"required,oneof=google"`
Source string `json:"source,optional,options=manual|zitadel|ldap|scim" validate:"omitempty,oneof=manual zitadel ldap scim"`
Status string `form:"status,optional,options=open|close" validate:"omitempty,oneof=open close"`
規則:只要有 oneof=...,就一定要在同欄位 tag 加對應 options=...,並把可選值也寫進行末註解。
4. 修改流程
每次改 .api 後必跑:
make gen-api # 同步 internal/types/types.go(json tag 帶 options= 給 go-zero runtime)
make gen-doc # 同步 docs/openapi/gateway.yaml(gitignore,本地驗證用)
go build ./... # 確保編譯通過
驗證:把 docs/openapi/gateway.yaml 丟進 https://editor.swagger.io 或 Swagger UI,確認分組、description、enum 都正確顯示。
Middleware(go-zero 正規手段)
強制原則:所有需要登入 / 授權的 endpoint 都用 .api 的 middleware: 宣告,不要在 gateway.go 用 server.Use(...) 全域掛載。
設定方式
在 @server 加 middleware: AuthJWT 或 middleware: AuthJWT,CasbinRBAC(多個用逗號):
// 公開(不掛)— 註冊 / 登入 / token refresh / health
@server (
group: auth
prefix: /api/v1/auth
tags: "Auth - 認證(公開)"
)
service gateway { ... register / login / token/exchange / token/refresh ... }
// 需登入(AuthJWT)— logout / member 自身資料 / Permission catalog & me
@server (
group: auth
prefix: /api/v1/auth
middleware: AuthJWT
tags: "Auth - 認證(需 Bearer)"
)
service gateway { ... logout ... }
// 需登入 + RBAC(AuthJWT,CasbinRBAC)— Permission 管理類 endpoint
@server (
group: permission
prefix: /api/v1/permissions
middleware: AuthJWT,CasbinRBAC
tags: "Permission - 權限(管理)"
)
service gateway { ... roles / role-mappings / users / policy/reload ... }
跑 make gen-api 後 internal/handler/routes.go 會自動產生:
server.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{serverCtx.AuthJWT, serverCtx.CasbinRBAC},
[]rest.Route{ ... }...,
),
rest.WithPrefix("/api/v1/permissions"),
)
撰寫新 middleware(go-zero 標準 struct 模式)
每個 middleware 必須是 struct + NewXxxMiddleware(...) 建構函式 + Handle(next http.HandlerFunc) http.HandlerFunc 方法。範例:
// internal/middleware/authjwt_middleware.go ← 檔名由 goctl stringx 決定
package middleware
type AuthJWTMiddleware struct {
tokens domauth.TokenUseCase
}
func NewAuthJWTMiddleware(tokens domauth.TokenUseCase) *AuthJWTMiddleware {
return &AuthJWTMiddleware{tokens: tokens}
}
func (m *AuthJWTMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// ...檢查 / 注入 actor / 呼叫 next(w, r)
}
}
檔名規則:goctl 會依 .api 的 middleware: 名字產生 stub 檔,命名 = stringx.From(<Name>).ToLower() + "_middleware.go",例如:
AuthJWT→authjwt_middleware.goCasbinRBAC→casbinrbac_middleware.go
新 middleware 必須直接用這個檔名寫實作,goctl 下次 gen-api 才會跳過(看到「已存在」)。
ServiceContext 注入
在 internal/svc/service_context.go 暴露 rest.Middleware 欄位並在 NewServiceContext 結尾 wire 起來:
type ServiceContext struct {
...
AuthJWT rest.Middleware
CasbinRBAC rest.Middleware
}
func NewServiceContext(c config.Config) *ServiceContext {
...
// 結尾,所有 use case 都建好之後
sc.AuthJWT = middleware.NewAuthJWTMiddleware(sc.AuthToken).Handle
sc.CasbinRBAC = middleware.NewCasbinRBACMiddleware(sc.PermissionRBAC, middleware.CasbinRBACOptions{}).Handle
return sc
}
.Handle 是 method value(func(next http.HandlerFunc) http.HandlerFunc),剛好符合 rest.Middleware 簽名。
Actor context 共享
JWT middleware 解析完 token 後用 internal/library/actor.WithActor(ctx, tenantID, uid) 注入 actor。
所有層(logic / usecase / 其他 middleware)讀 actor 都用 actor.ActorFromContext(ctx) —— 不要在各 logic 套件各自定義 actorKey struct{},否則 context value 進不來。
internal/logic/member/actor.go 與 internal/logic/permission/actor.go 是 library/actor 的 thin alias,方便既有程式用 member.Actor / member.ActorFromContext,但底層 key 是同一個。
與 runtime 對齊
Handler 使用 response.Write 輸出:
{ "code": 102000, "message": "SUCCESS", "data": { ... } }
失敗時含 error.biz_code / error.scope 等欄位。Handler parse 錯誤為 Facade scope(10101000);各模組 logic/usecase 使用對應 scope(Auth=28、Member=29)。