198 lines
4.7 KiB
Go
198 lines
4.7 KiB
Go
package middleware
|
||
|
||
import (
|
||
"errors"
|
||
"net/http"
|
||
"slices"
|
||
"strings"
|
||
|
||
"hyapp-admin-server/internal/appctx"
|
||
"hyapp-admin-server/internal/config"
|
||
"hyapp-admin-server/internal/repository"
|
||
"hyapp-admin-server/internal/response"
|
||
"hyapp-admin-server/internal/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type AppAccessStore interface {
|
||
AppAccessForUser(userID uint) (repository.AppAccess, error)
|
||
}
|
||
|
||
const (
|
||
ContextUserID = "userID"
|
||
ContextUsername = "username"
|
||
ContextPermissions = "permissions"
|
||
)
|
||
|
||
func CORS(cfg config.Config) gin.HandlerFunc {
|
||
allowed := map[string]struct{}{}
|
||
for _, origin := range cfg.CORSAllowedOrigins {
|
||
allowed[origin] = struct{}{}
|
||
}
|
||
|
||
return func(c *gin.Context) {
|
||
origin := c.Request.Header.Get("Origin")
|
||
if _, ok := allowed[origin]; ok {
|
||
c.Header("Access-Control-Allow-Origin", origin)
|
||
c.Header("Vary", "Origin")
|
||
c.Header("Access-Control-Allow-Credentials", "true")
|
||
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Request-ID, "+appctx.HeaderAppCode)
|
||
c.Header("Access-Control-Expose-Headers", "X-Request-ID")
|
||
c.Header("Access-Control-Allow-Methods", "GET, POST, PATCH, PUT, DELETE, OPTIONS")
|
||
}
|
||
|
||
if c.Request.Method == http.MethodOptions {
|
||
c.AbortWithStatus(http.StatusNoContent)
|
||
return
|
||
}
|
||
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
func AppCode() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
appCode := appctx.Normalize(c.GetHeader(appctx.HeaderAppCode))
|
||
c.Request = c.Request.WithContext(appctx.WithContext(c.Request.Context(), appCode))
|
||
c.Header(appctx.HeaderAppCode, appCode)
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
func AuthRequired(auth *service.AuthService) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
header := c.GetHeader("Authorization")
|
||
if !strings.HasPrefix(header, "Bearer ") {
|
||
response.Unauthorized(c, "缺少访问凭证")
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
claims, err := auth.ParseAccessToken(strings.TrimPrefix(header, "Bearer "))
|
||
if err != nil {
|
||
response.Unauthorized(c, "访问凭证已失效")
|
||
c.Abort()
|
||
return
|
||
}
|
||
|
||
c.Set(ContextUserID, claims.UserID)
|
||
c.Set(ContextUsername, claims.Username)
|
||
c.Set(ContextPermissions, claims.Permissions)
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
func RequireAppScope(store AppAccessStore) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// App 级路由必须显式携带 header,不能复用 appctx 的 lalu 默认值;否则无 App 用户省略 header 仍会落入 Lalu 数据域。
|
||
appCode := strings.ToLower(strings.TrimSpace(c.GetHeader(appctx.HeaderAppCode)))
|
||
if appCode == "" {
|
||
response.Forbidden(c, "当前用户没有可访问的 App")
|
||
c.Abort()
|
||
return
|
||
}
|
||
if store == nil {
|
||
response.ServerError(c, "App 权限服务不可用")
|
||
c.Abort()
|
||
return
|
||
}
|
||
access, err := store.AppAccessForUser(CurrentUserID(c))
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
response.Unauthorized(c, "用户不存在")
|
||
} else {
|
||
response.ServerError(c, "校验 App 权限失败")
|
||
}
|
||
c.Abort()
|
||
return
|
||
}
|
||
if !access.Allows(appCode) {
|
||
response.Forbidden(c, "当前用户无权访问该 App")
|
||
c.Abort()
|
||
return
|
||
}
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
func RequirePermission(code string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
if HasPermission(c, code) {
|
||
c.Next()
|
||
return
|
||
}
|
||
|
||
response.Forbidden(c, "没有操作权限")
|
||
c.Abort()
|
||
}
|
||
}
|
||
|
||
// RequireAnyPermission is used by transitional RBAC routes where the new
|
||
// button-level permission and the old broad permission must both work.
|
||
func RequireAnyPermission(codes ...string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
if HasAnyPermission(c, codes...) {
|
||
c.Next()
|
||
return
|
||
}
|
||
|
||
response.Forbidden(c, "没有操作权限")
|
||
c.Abort()
|
||
}
|
||
}
|
||
|
||
// HasAnyPermission keeps privilege checks in handlers readable when one action
|
||
// is allowed by multiple compatible permission codes.
|
||
func HasAnyPermission(c *gin.Context, codes ...string) bool {
|
||
for _, code := range codes {
|
||
if HasPermission(c, code) {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// HasPermission reads only the JWT-derived permission list. Database state is
|
||
// refreshed by login/refresh, so every request uses one consistent permission snapshot.
|
||
func HasPermission(c *gin.Context, code string) bool {
|
||
return slices.Contains(CurrentPermissions(c), code)
|
||
}
|
||
|
||
func CurrentUserID(c *gin.Context) uint {
|
||
value, ok := c.Get(ContextUserID)
|
||
if !ok {
|
||
return 0
|
||
}
|
||
id, ok := value.(uint)
|
||
if !ok {
|
||
return 0
|
||
}
|
||
return id
|
||
}
|
||
|
||
func CurrentUsername(c *gin.Context) string {
|
||
value, ok := c.Get(ContextUsername)
|
||
if !ok {
|
||
return ""
|
||
}
|
||
username, ok := value.(string)
|
||
if !ok {
|
||
return ""
|
||
}
|
||
return username
|
||
}
|
||
|
||
func CurrentPermissions(c *gin.Context) []string {
|
||
value, ok := c.Get(ContextPermissions)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
permissions, ok := value.([]string)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return permissions
|
||
}
|