feat(copilot): finish configuration center

This commit is contained in:
2026-07-13 14:57:28 +08:00
parent 0a69d80f7c
commit 8b9eedc0e2
60 changed files with 3040 additions and 1042 deletions
+1
View File
@@ -53,6 +53,7 @@ func AuthMiddlewareWithService(jwtSvc *auth.JWTService) gin.HandlerFunc {
c.Set("user_id", claims.UserID)
c.Set("account_id", claims.AccountID)
c.Set("role", claims.Role)
c.Set("user_type", claims.UserType)
c.Set("provider", claims.Provider)
c.Set("custom_role_id", claims.CustomRoleID)
c.Set("claims", claims) // full Claims struct for handlers that need it
+27
View File
@@ -118,6 +118,33 @@ func TestAuthMiddleware_ChatwootAccessTokenHeader(t *testing.T) {
assert.Equal(t, 200, w.Code)
}
func TestAuthMiddleware_AllowsPlatformAdminThroughSuperAdminGuard(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := makeJWTConfig()
jwtService := auth.NewJWTService(cfg)
user := &model.User{
Base: model.Base{ID: 1},
Provider: "email",
Role: "super_admin",
Type: "User",
}
pair, err := jwtService.GenerateTokenPair(user, 2, "administrator")
assert.NoError(t, err)
r := gin.New()
r.Use(AuthMiddleware(cfg), SuperAdmin())
r.GET("/test", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true})
})
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.Header.Set("access-token", pair.AccessToken)
r.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
}
func TestAuthMiddleware_FallbackHeaders(t *testing.T) {
gin.SetMode(gin.TestMode)
cfg := makeJWTConfig()
+20 -23
View File
@@ -6,6 +6,7 @@ package middleware
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
@@ -17,30 +18,17 @@ import (
// Super admins are platform-level administrators that can manage all accounts,
// platform apps, and system configuration.
//
// This checks the user's type field (not the account-level role).
// user.type = "super_admin" is set at the User model level, not AccountUser.
// This checks the signed platform user type (not the account-level role).
//
// Usage:
// router.GET("/platform/accounts", SuperAdmin(), listAllAccounts)
// router.POST("/platform/apps", SuperAdmin(), createPlatformApp)
// router.GET("/platform/analytics", SuperAdmin(), viewPlatformAnalytics)
//
// router.GET("/platform/accounts", SuperAdmin(), listAllAccounts)
// router.POST("/platform/apps", SuperAdmin(), createPlatformApp)
// router.GET("/platform/analytics", SuperAdmin(), viewPlatformAnalytics)
func SuperAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
// Check for super_admin flag in context (set by AuthRequired middleware)
userType, exists := c.Get("user_type")
if !exists {
// No user_type in context — check claims for super_admin indication
_, claimsExists := c.Get("auth_claims")
if claimsExists {
// Claims exist but user_type not set — not super_admin
}
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden,
"Super admin access required")
return
}
typeStr, ok := userType.(string)
if !ok || typeStr != "super_admin" {
if !exists || !isSuperAdminType(userType) {
response.AbortWithStatusError(c, http.StatusForbidden, response.ErrForbidden,
"Super admin access required. Your account does not have platform administration privileges.")
return
@@ -57,14 +45,14 @@ func SuperAdmin() gin.HandlerFunc {
// Useful for endpoints that should be accessible to account admins and platform admins.
//
// Usage:
// router.DELETE("/accounts/:id", SuperAdminOrAdministrator(), deleteAccount)
//
// router.DELETE("/accounts/:id", SuperAdminOrAdministrator(), deleteAccount)
func SuperAdminOrAdministrator() gin.HandlerFunc {
return func(c *gin.Context) {
// Check super_admin first
userType, exists := c.Get("user_type")
if exists {
typeStr, ok := userType.(string)
if ok && typeStr == "super_admin" {
if isSuperAdminType(userType) {
c.Set("is_super_admin", true)
c.Next()
return
@@ -94,4 +82,13 @@ func SuperAdminOrAdministrator() gin.HandlerFunc {
c.Next()
}
}
}
func isSuperAdminType(value any) bool {
typeStr, ok := value.(string)
if !ok {
return false
}
normalized := strings.ToLower(strings.ReplaceAll(strings.TrimSpace(typeStr), "_", ""))
return normalized == "superadmin"
}
@@ -47,6 +47,19 @@ func TestSuperAdmin_IsSuperAdmin(t *testing.T) {
assert.Equal(t, 200, w.Code)
}
func TestSuperAdmin_AcceptsChatwootSerializedType(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
r.Use(func(c *gin.Context) { c.Set("user_type", "SuperAdmin"); c.Next() })
r.Use(SuperAdmin())
r.GET("/test", func(c *gin.Context) { c.JSON(200, gin.H{"ok": true}) })
w := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodGet, "/test", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}
func TestSuperAdmin_ClaimsExistButNoType(t *testing.T) {
gin.SetMode(gin.TestMode)
r := gin.New()
@@ -58,4 +71,4 @@ func TestSuperAdmin_ClaimsExistButNoType(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/test", nil)
r.ServeHTTP(w, req)
assert.Equal(t, 403, w.Code)
}
}