thread-master/apps/backend/internal/middleware/auth.go

98 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package middleware
import (
"context"
"net/http"
"strings"
"apps/backend/internal/domain"
"apps/backend/internal/module/ai"
memberDomain "apps/backend/internal/module/member/domain"
"apps/backend/internal/module/permission"
tokenDomain "apps/backend/internal/module/token/domain"
"apps/backend/internal/response"
)
type ctxKey int
const (
ctxUID ctxKey = iota + 1
ctxRoles
ctxMember
)
func UIDFrom(ctx context.Context) (int64, bool) {
v, ok := ctx.Value(ctxUID).(int64)
return v, ok
}
// WithUID 讓 logic 層測試能組出「已登入」的 ctx。ctxUID 是私有型別,
// 沒有這個入口就只能繞過 UIDFrom 驗權,那測到的就不是真的授權路徑了。
func WithUID(ctx context.Context, uid int64) context.Context {
return context.WithValue(ctx, ctxUID, uid)
}
func RolesFrom(ctx context.Context) []string {
v, _ := ctx.Value(ctxRoles).([]string)
return v
}
// AuthMiddleware validates Bearer access JWT.
func AuthMiddleware(issuer tokenDomain.Issuer, store memberDomain.Repository) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h := r.Header.Get("Authorization")
if h == "" || !strings.HasPrefix(strings.ToLower(h), "bearer ") {
response.Fail(w, http.StatusUnauthorized, 401001, "missing authorization", nil)
return
}
token := strings.TrimSpace(h[7:])
claims, err := issuer.ParseAccess(token)
if err != nil {
response.Fail(w, http.StatusUnauthorized, 401002, "invalid or expired token", nil)
return
}
m, err := store.FindByUID(r.Context(), claims.UID)
if err != nil {
response.Fail(w, http.StatusUnauthorized, 401003, "member not found", nil)
return
}
if m.IsSuspended() {
response.Fail(w, http.StatusForbidden, 403001, "account suspended", nil)
return
}
ctx := context.WithValue(r.Context(), ctxUID, m.UID)
ctx = context.WithValue(ctx, ctxRoles, m.Roles)
ctx = context.WithValue(ctx, ctxMember, m)
ctx = context.WithValue(ctx, domain.UIDCode, m.UID)
// LLM 系統 prompt 語言:會員 preferred_language > Accept-Language > 繁中
ctx = ai.WithResponseLanguage(ctx, ai.PickLanguage(m.PreferredLanguage, r.Header.Get("Accept-Language")))
next(w, r.WithContext(ctx))
}
}
}
// AdminMiddleware requires permission.AdminAccessadmin 角色預設授予).
func AdminMiddleware(next http.HandlerFunc) http.HandlerFunc {
return RequirePermission(permission.AdminAccess)(next)
}
// RequirePermission 檢查 JWT roles 是否具備指定能力點(須在 AuthJWT 之後).
func RequirePermission(code string) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
roles := RolesFrom(r.Context())
if !permission.Has(roles, code) {
response.Fail(w, http.StatusForbidden, 403002, "permission denied: "+code, nil)
return
}
next(w, r)
}
}
}
func MemberFrom(ctx context.Context) (*memberDomain.Member, bool) {
m, ok := ctx.Value(ctxMember).(*memberDomain.Member)
return m, ok
}