chore: initial commit of ai site platform

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
whm
2026-07-31 10:31:17 +08:00
commit 4ca82fb58a
203 changed files with 45745 additions and 0 deletions

View File

@@ -0,0 +1,396 @@
package authx
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
"github.com/zeromicro/go-zero/rest/httpx"
)
type ctxKey string
const (
CtxTenantID ctxKey = "tenant_id"
CtxUserID ctxKey = "user_id"
CtxRole ctxKey = "role"
CtxAgentID ctxKey = "agent_id"
CtxPerms ctxKey = "perms"
CtxOrgUnitID ctxKey = "org_unit_id"
)
// 平台级角色 → 权限映射(角色、权限一律中文命名)
var RolePermissions = map[string][]string{
Role超级管理员: {
Perm管理租户, // 只管公司一级:列表/新建等;不介入公司内模块、成员、同步
},
Role管理员: {
Perm读取模块, Perm写入模块, Perm发布模块,
Perm新增数据, Perm查询数据, Perm更新数据, Perm删除数据, Perm导出数据, Perm导入数据,
Perm查看审计, Perm上传文件, Perm下载文件,
Perm管理智能体, Perm邀请成员, Perm管理组织,
Perm数据同步,
},
Role编辑: {
Perm读取模块, Perm写入模块,
Perm新增数据, Perm查询数据, Perm更新数据, Perm导出数据, Perm导入数据,
Perm上传文件, Perm下载文件,
},
Role只读: {
Perm读取模块, Perm查询数据, Perm导出数据, Perm下载文件, Perm查看审计,
},
Role待加入: {},
}
type Claims struct {
TenantID int64 `json:"tenant_id"`
UserID int64 `json:"user_id"`
Role string `json:"role,omitempty"`
OrgUnitID int64 `json:"org_unit_id,omitempty"`
AgentID int64 `json:"agent_id,omitempty"`
Perms []string `json:"perms,omitempty"`
jwt.RegisteredClaims
}
type JWTConfig struct {
AccessSecret string
AccessExpire int64
}
func (c JWTConfig) Expire() time.Duration {
if c.AccessExpire <= 0 {
return 24 * time.Hour
}
return time.Duration(c.AccessExpire) * time.Second
}
func IssueToken(cfg JWTConfig, tenantID, userID int64, role string, orgUnitID int64) (string, int64, error) {
if cfg.AccessSecret == "" {
return "", 0, errors.New("auth access secret empty")
}
if userID <= 0 {
return "", 0, errors.New("user_id required")
}
if role == "" {
role = Role管理员
}
role = NormalizeRole(role)
if _, ok := RolePermissions[role]; !ok {
return "", 0, fmt.Errorf("unknown role: %s", role)
}
// 待加入 / 超级管理员可无租户;其他角色必须带 tenant_id
if tenantID <= 0 && role != Role待加入 && role != Role超级管理员 {
return "", 0, errors.New("tenant_id and user_id required")
}
if tenantID < 0 {
tenantID = 0
}
return signClaims(cfg, Claims{
TenantID: tenantID,
UserID: userID,
Role: role,
OrgUnitID: orgUnitID,
})
}
// IssueAgentToken 签发智能体 JWTrole=agent权限写在 perms 声明中)。
func IssueAgentToken(cfg JWTConfig, tenantID, agentID int64, perms []string) (string, int64, error) {
if cfg.AccessSecret == "" {
return "", 0, errors.New("auth access secret empty")
}
if tenantID <= 0 || agentID <= 0 {
return "", 0, errors.New("tenant_id and agent_id required")
}
return signClaims(cfg, Claims{
TenantID: tenantID,
UserID: agentID,
Role: RoleAgent,
AgentID: agentID,
Perms: NormalizePerms(perms),
})
}
func signClaims(cfg JWTConfig, claims Claims) (string, int64, error) {
exp := time.Now().Add(cfg.Expire())
claims.RegisteredClaims = jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(exp),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "aijianzhan-platform",
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
signed, err := token.SignedString([]byte(cfg.AccessSecret))
if err != nil {
return "", 0, err
}
return signed, exp.Unix(), nil
}
func ParseToken(cfg JWTConfig, tokenStr string) (*Claims, error) {
if cfg.AccessSecret == "" {
return nil, errors.New("auth access secret empty")
}
parser := jwt.NewParser(jwt.WithValidMethods([]string{"HS256"}))
token, err := parser.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) {
return []byte(cfg.AccessSecret), nil
})
if err != nil {
return nil, err
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, errors.New("invalid token")
}
if claims.UserID <= 0 {
return nil, errors.New("token missing user")
}
if claims.Role == "" {
claims.Role = Role只读
}
claims.Role = NormalizeRole(claims.Role)
if claims.TenantID <= 0 && claims.Role != Role待加入 && claims.Role != Role超级管理员 {
return nil, errors.New("token missing tenant/user")
}
if claims.TenantID < 0 {
claims.TenantID = 0
}
if claims.Role == Role智能体 && claims.AgentID <= 0 {
claims.AgentID = claims.UserID
}
return claims, nil
}
func Middleware(jwtCfg JWTConfig, devAuth bool) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
if strings.HasPrefix(strings.ToLower(auth), "bearer ") {
raw := strings.TrimSpace(auth[7:])
claims, err := ParseToken(jwtCfg, raw)
if err != nil {
WriteError(w, http.StatusUnauthorized, "invalid token: "+err.Error())
return
}
ctx := WithFullClaims(r.Context(), claims.TenantID, claims.UserID, claims.Role, claims.AgentID, claims.OrgUnitID, claims.Perms)
next(w, r.WithContext(ctx))
return
}
if devAuth {
tenantID, _ := strconv.ParseInt(r.Header.Get("X-Tenant-Id"), 10, 64)
userID, _ := strconv.ParseInt(r.Header.Get("X-User-Id"), 10, 64)
role := r.Header.Get("X-Role")
if tenantID <= 0 {
tenantID = 1
}
if userID <= 0 {
userID = 1
}
if role == "" {
role = Role管理员
}
role = NormalizeRole(role)
ctx := WithClaims(r.Context(), tenantID, userID, role)
next(w, r.WithContext(ctx))
return
}
WriteError(w, http.StatusUnauthorized, "missing Authorization Bearer token")
}
}
}
// RequirePermission 校验平台角色或智能体 JWT 内嵌权限。
func RequirePermission(perm string) func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !HasPermissionCtx(r.Context(), perm) {
role := Role(r.Context())
WriteError(w, http.StatusForbidden, "permission denied: "+perm+" (role="+role+")")
return
}
next(w, r)
}
}
}
// RequireTenant 要求已加入租户pending / 无 tenant 不可访问业务数据。
// 超级管理员可在「进入某公司」后携带 tenant_id 访问公司内接口(可看可改,前端会多重确认)。
func RequireTenant() func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if Role(r.Context()) == Role智能体 {
if TenantID(r.Context()) <= 0 {
WriteError(w, http.StatusForbidden, "agent missing tenant")
return
}
next(w, r)
return
}
if Role(r.Context()) == Role超级管理员 {
if TenantID(r.Context()) <= 0 {
WriteError(w, http.StatusForbidden, "请先在平台管理中进入某公司后再操作公司内部功能")
return
}
next(w, r)
return
}
if TenantID(r.Context()) <= 0 || Role(r.Context()) == Role待加入 {
WriteError(w, http.StatusForbidden, "join a tenant first (pending membership)")
return
}
next(w, r)
}
}
}
// RequirePlatformAdmin 仅平台超级管理员。
func RequirePlatformAdmin() func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !IsPlatformAdmin(Role(r.Context())) {
WriteError(w, http.StatusForbidden, "需要超级管理员")
return
}
next(w, r)
}
}
}
// RequireTenantBound 保留兼容:业务接口要求已绑定租户。
func RequireTenantBound() func(http.HandlerFunc) http.HandlerFunc {
return func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if IsPlatformAdmin(Role(r.Context())) {
if TenantID(r.Context()) <= 0 {
WriteError(w, http.StatusForbidden, "请先进入某公司")
return
}
next(w, r)
return
}
if TenantID(r.Context()) <= 0 {
WriteError(w, http.StatusForbidden, "请先加入公司")
return
}
next(w, r)
}
}
}
func HasPermission(role, perm string) bool {
perms, ok := RolePermissions[NormalizeRole(role)]
if !ok {
return false
}
want := NormalizePerm(perm)
for _, p := range perms {
if NormalizePerm(p) == want {
return true
}
}
return false
}
func HasPermissionCtx(ctx context.Context, perm string) bool {
role := NormalizeRole(Role(ctx))
want := NormalizePerm(perm)
if role == Role超级管理员 {
// 进入公司后:超管可使用全部公司权限(平台专属「管理租户」仍保留)
if want == Perm管理租户 {
return true
}
for _, p := range CompanyPermCatalog() {
if NormalizePerm(p) == want {
return TenantID(ctx) > 0
}
}
return HasPermission(role, want)
}
if role == Role智能体 {
ok := false
for _, p := range Perms(ctx) {
if NormalizePerm(p) == want {
ok = true
break
}
}
if !ok {
return false
}
} else if !HasPermission(role, want) {
return false
}
tid := TenantID(ctx)
if tid > 0 && EntitlementChecker != nil {
if !EntitlementChecker(ctx, tid, want) {
return false
}
}
return true
}
func withClaims(ctx context.Context, tenantID, userID int64, role string) context.Context {
return WithClaims(ctx, tenantID, userID, role)
}
// WithClaims 写入租户/用户/角色(公开展示等场景用)。
func WithClaims(ctx context.Context, tenantID, userID int64, role string) context.Context {
return WithFullClaims(ctx, tenantID, userID, role, 0, 0, nil)
}
func WithFullClaims(ctx context.Context, tenantID, userID int64, role string, agentID, orgUnitID int64, perms []string) context.Context {
ctx = context.WithValue(ctx, CtxTenantID, tenantID)
ctx = context.WithValue(ctx, CtxUserID, userID)
ctx = context.WithValue(ctx, CtxRole, role)
ctx = context.WithValue(ctx, CtxAgentID, agentID)
ctx = context.WithValue(ctx, CtxOrgUnitID, orgUnitID)
if perms != nil {
ctx = context.WithValue(ctx, CtxPerms, append([]string{}, perms...))
}
return ctx
}
func TenantID(ctx context.Context) int64 {
v, _ := ctx.Value(CtxTenantID).(int64)
return v
}
func UserID(ctx context.Context) int64 {
v, _ := ctx.Value(CtxUserID).(int64)
return v
}
func AgentID(ctx context.Context) int64 {
v, _ := ctx.Value(CtxAgentID).(int64)
return v
}
func OrgUnitID(ctx context.Context) int64 {
v, _ := ctx.Value(CtxOrgUnitID).(int64)
return v
}
func Role(ctx context.Context) string {
v, _ := ctx.Value(CtxRole).(string)
if v == "" {
return Role只读
}
return NormalizeRole(v)
}
func Perms(ctx context.Context) []string {
v, _ := ctx.Value(CtxPerms).([]string)
return v
}
func WriteError(w http.ResponseWriter, code int, msg string) {
httpx.WriteJson(w, code, map[string]any{
"code": code,
"message": msg,
})
}