template-monorepo/internal/middleware/authjwt_middleware.go

78 lines
2.5 KiB
Go
Raw Permalink Normal View History

refactor(middleware): wire AuthJWT + CasbinRBAC via .api middleware directive Stop relying on a global server.Use(CloudEPJWT) that was invisible from the .api source. Protected routes now declare middleware explicitly in each @server block and goctl chains them into routes.go — the .api file is the single source of truth for "who needs Bearer / who needs RBAC". Concretely: - Rewrite middleware to go-zero's standard struct + Handle() pattern. AuthJWT becomes strict: missing/invalid Bearer returns 28501000 (was soft passthrough). CasbinRBAC stays nil-tolerant so dev/test boots without a policy. - Files renamed to goctl's stringx convention (authjwt_middleware.go, casbinrbac_middleware.go) so future `make gen-api` runs see them as already-generated and skip the empty stub. - Move actor context helpers (Actor, WithActor, ActorFromContext) into internal/library/actor so middleware and BOTH logic packages share one context key. Previously each logic package had its own private actorKey struct{}, so an actor injected for member was invisible to permission — the permission RBAC chain would always see "missing actor". member/permission actor.go are now thin type-alias shims. - .api files declare middleware per group: auth.api (public) → no middleware (register/login/token/...) auth.api (logout) → middleware: AuthJWT member.api → middleware: AuthJWT permission.api (catalog,me) → middleware: AuthJWT permission.api (admin ops) → middleware: AuthJWT,CasbinRBAC normal.api (/health) → no middleware - ServiceContext exposes AuthJWT / CasbinRBAC as rest.Middleware; the global server.Use(...) in gateway.go is removed. - Document the pattern in AGENTS.md (cross-agent rules) and generate/api/README.md (detailed examples + filename rules) so any future AI agent or human follows the same convention. make gen-api / gen-doc / lint / build all pass. Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-21 09:30:50 +00:00
package middleware
import (
"net/http"
"strings"
"gateway/internal/library/actor"
errs "gateway/internal/library/errors"
"gateway/internal/library/errors/code"
domauth "gateway/internal/model/auth/domain/usecase"
"gateway/internal/response"
)
// AuthJWTMiddleware enforces Bearer access tokens on protected routes.
//
// Mounted via @server(middleware: AuthJWT) in the .api file. Missing or
// invalid tokens return 28501000 (Auth scope, unauthorized) — public
// routes (register / login / token refresh / health) must NOT mount
// this middleware.
//
// On success the parsed (tenant, uid) is injected via library/actor
// so downstream logic can read it through actor.ActorFromContext (or
// the package-local member.ActorFromContext / permission.ActorFromContext
// aliases).
//
// File name follows goctl's stringx convention (`authjwt_middleware.go`)
// so `make gen-api` sees it as already-generated and never overwrites
// the implementation.
type AuthJWTMiddleware struct {
tokens domauth.TokenUseCase
}
// NewAuthJWTMiddleware wires the middleware with the auth module's
// TokenUseCase (set up in ServiceContext.NewServiceContext).
func NewAuthJWTMiddleware(tokens domauth.TokenUseCase) *AuthJWTMiddleware {
return &AuthJWTMiddleware{tokens: tokens}
}
// Handle implements the go-zero rest.Middleware signature.
func (m *AuthJWTMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
bld := errs.For(code.Auth)
return func(w http.ResponseWriter, r *http.Request) {
if m.tokens == nil {
response.Write(r.Context(), w, nil,
bld.SysNotImplemented("auth middleware: token usecase not configured"))
return
}
raw := bearerToken(r.Header.Get("Authorization"))
if raw == "" {
response.Write(r.Context(), w, nil,
bld.AuthUnauthorized("missing bearer token"))
return
}
claims, err := m.tokens.ParseAccessToken(r.Context(), raw)
if err != nil {
// surface already-typed Auth errors as-is so the biz code
// (e.g. expired vs invalid) is preserved.
if e := errs.FromError(err); e != nil && e.Category() == code.AuthUnauthorized {
response.Write(r.Context(), w, nil, err)
return
}
response.Write(r.Context(), w, nil,
bld.AuthUnauthorized("invalid bearer token").WithCause(err))
return
}
ctx := actor.WithActor(r.Context(), claims.TenantID, claims.UID)
next(w, r.WithContext(ctx))
}
}
func bearerToken(header string) string {
const prefix = "Bearer "
if !strings.HasPrefix(header, prefix) {
return ""
}
return strings.TrimSpace(strings.TrimPrefix(header, prefix))
}