395 lines
11 KiB
Go
395 lines
11 KiB
Go
package controllers
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"server/models"
|
||
"server/pkg/jwtutil"
|
||
|
||
beego "github.com/beego/beego/v2/server/web"
|
||
)
|
||
|
||
// BackendRoleController 租户端(backend)角色管理(yz_system_admin_role, cid=2)
|
||
// 角色按租户隔离:所有读写均以当前登录租户(JWT 中的 tenant_id)为边界,
|
||
// 租户仅能查看/管理自己名下的角色,无法越权访问其他租户数据。
|
||
type BackendRoleController struct {
|
||
beego.Controller
|
||
}
|
||
|
||
type backendRolePayload struct {
|
||
Name string `json:"name"`
|
||
Status *uint8 `json:"status"`
|
||
Rights interface{} `json:"rights"`
|
||
}
|
||
|
||
// parseSubmittedIDs 将前端提交的 rights 解析为菜单 ID 列表(兼容 JSON 数组 / 逗号分隔字符串)。
|
||
func parseSubmittedIDs(v interface{}) []uint64 {
|
||
ids := make([]uint64, 0)
|
||
switch t := v.(type) {
|
||
case []interface{}:
|
||
for _, item := range t {
|
||
switch n := item.(type) {
|
||
case float64:
|
||
ids = append(ids, uint64(n))
|
||
case json.Number:
|
||
if i, err := n.Int64(); err == nil {
|
||
ids = append(ids, uint64(i))
|
||
}
|
||
case string:
|
||
if i, err := strconv.ParseUint(strings.TrimSpace(n), 10, 64); err == nil {
|
||
ids = append(ids, i)
|
||
}
|
||
}
|
||
}
|
||
case []uint64:
|
||
ids = append(ids, t...)
|
||
case string:
|
||
var arr []uint64
|
||
if err := json.Unmarshal([]byte(t), &arr); err == nil {
|
||
ids = append(ids, arr...)
|
||
} else {
|
||
for _, part := range strings.Split(t, ",") {
|
||
if i, err := strconv.ParseUint(strings.TrimSpace(part), 10, 64); err == nil {
|
||
ids = append(ids, i)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// loadAssignableTenantMenuIDs 返回租户端可分配菜单的 ID 集合:cid=2 且已启用且已显示。
|
||
func loadAssignableTenantMenuIDs() map[uint64]bool {
|
||
ids := make(map[uint64]bool)
|
||
var menus []models.SystemMenu
|
||
if _, err := models.Orm.QueryTable(new(models.SystemMenu)).All(&menus); err != nil {
|
||
return ids
|
||
}
|
||
menus = filterMenusByView(menus, 2)
|
||
menus = filterAssignableMenus(menus)
|
||
for _, m := range menus {
|
||
ids[m.ID] = true
|
||
}
|
||
return ids
|
||
}
|
||
|
||
// sanitizeTenantRights 在服务端收敛租户角色权限:
|
||
// 仅保留租户端(cid=2)已启用且已显示的菜单 ID,剔除平台菜单、停用菜单、隐藏菜单。
|
||
// rights 为 nil 或空串表示全权限(与 filterMenusByRights 保持一致)。
|
||
func sanitizeTenantRights(v interface{}) *string {
|
||
if v == nil {
|
||
return nil
|
||
}
|
||
if s, ok := v.(string); ok && strings.TrimSpace(s) == "" {
|
||
return nil
|
||
}
|
||
|
||
submitted := parseSubmittedIDs(v)
|
||
empty := "[]"
|
||
if len(submitted) == 0 {
|
||
return &empty
|
||
}
|
||
|
||
allowed := loadAssignableTenantMenuIDs()
|
||
kept := make([]uint64, 0, len(submitted))
|
||
seen := make(map[uint64]bool, len(submitted))
|
||
for _, id := range submitted {
|
||
if id == 0 || seen[id] || !allowed[id] {
|
||
continue
|
||
}
|
||
seen[id] = true
|
||
kept = append(kept, id)
|
||
}
|
||
if len(kept) == 0 {
|
||
return &empty
|
||
}
|
||
b, _ := json.Marshal(kept)
|
||
s := string(b)
|
||
return &s
|
||
}
|
||
|
||
// currentTenantID 从 Bearer Token 解析当前登录租户 ID。
|
||
// 返回 (tid, true) 表示成功;失败时已写入 401/403 响应并返回 (0, false)。
|
||
func (c *BackendRoleController) currentTenantID() (uint64, bool) {
|
||
authHeader := c.Ctx.Request.Header.Get("Authorization")
|
||
if authHeader == "" {
|
||
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "未登录"}
|
||
_ = c.ServeJSON()
|
||
return 0, false
|
||
}
|
||
authParts := strings.SplitN(authHeader, " ", 2)
|
||
if len(authParts) != 2 || authParts[0] != "Bearer" {
|
||
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "认证信息格式错误"}
|
||
_ = c.ServeJSON()
|
||
return 0, false
|
||
}
|
||
claims, err := jwtutil.ParseToken(authParts[1])
|
||
if err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "无效的token"}
|
||
_ = c.ServeJSON()
|
||
return 0, false
|
||
}
|
||
if claims.UserType != "backend" && claims.UserType != "app" {
|
||
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "无权访问"}
|
||
_ = c.ServeJSON()
|
||
return 0, false
|
||
}
|
||
if claims.TenantId <= 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 401, "msg": "租户信息缺失"}
|
||
_ = c.ServeJSON()
|
||
return 0, false
|
||
}
|
||
return uint64(claims.TenantId), true
|
||
}
|
||
|
||
// GetAllRoles 获取当前租户可见的角色列表
|
||
// GET /backend/allRoles
|
||
// 返回「全局角色(cid=2, tenant_id=0,所有租户共用)」+「本租户自建角色」;
|
||
// 全局角色由平台端统一维护,租户端只读。
|
||
func (c *BackendRoleController) GetAllRoles() {
|
||
tid, ok := c.currentTenantID()
|
||
if !ok {
|
||
return
|
||
}
|
||
var rows []models.AdminRole
|
||
_, err := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("cid", 2).
|
||
Filter("tenant_id__in", []uint64{0, tid}).
|
||
OrderBy("tenant_id", "is_custom", "id").
|
||
All(&rows)
|
||
if err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "查询失败"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": rows}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
// GetRoleByID 获取当前租户的角色详情
|
||
// GET /backend/roles/:id
|
||
func (c *BackendRoleController) GetRoleByID() {
|
||
tid, ok := c.currentTenantID()
|
||
if !ok {
|
||
return
|
||
}
|
||
idStr := c.Ctx.Input.Param(":id")
|
||
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||
if id == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
var role models.AdminRole
|
||
if err := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("id", id).
|
||
Filter("cid", 2).
|
||
Filter("tenant_id__in", []uint64{0, tid}).
|
||
One(&role); err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": role}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
// CreateRole 创建当前租户的角色
|
||
// POST /backend/roles
|
||
func (c *BackendRoleController) CreateRole() {
|
||
tid, ok := c.currentTenantID()
|
||
if !ok {
|
||
return
|
||
}
|
||
var p backendRolePayload
|
||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
if err := json.Unmarshal(raw, &p); err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
p.Name = strings.TrimSpace(p.Name)
|
||
if p.Name == "" {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "name 不能为空"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
// 不能与本租户可见的角色重名(含平台默认的全局角色),避免混淆
|
||
dup, derr := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("cid", 2).
|
||
Filter("tenant_id__in", []uint64{0, tid}).
|
||
Filter("name", p.Name).
|
||
Count()
|
||
if derr != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
if dup > 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "已存在同名角色(含平台默认角色),请更换名称"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
|
||
status := uint8(1)
|
||
if p.Status != nil {
|
||
status = *p.Status
|
||
}
|
||
rights := sanitizeTenantRights(p.Rights)
|
||
// 租户端创建的角色一律标记为“自定义角色”,与系统/平台预置角色区分,并按租户隔离
|
||
role := &models.AdminRole{
|
||
TenantID: tid,
|
||
Cid: 2,
|
||
IsCustom: 1,
|
||
Name: p.Name,
|
||
Status: status,
|
||
Rights: rights,
|
||
}
|
||
id, err := models.Orm.Insert(role)
|
||
if err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "创建失败"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success", "data": map[string]interface{}{"id": id}}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
// UpdateRole 更新当前租户的角色
|
||
// PUT /backend/roles/:id
|
||
func (c *BackendRoleController) UpdateRole() {
|
||
tid, ok := c.currentTenantID()
|
||
if !ok {
|
||
return
|
||
}
|
||
idStr := c.Ctx.Input.Param(":id")
|
||
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||
if id == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
|
||
var p backendRolePayload
|
||
raw, _ := io.ReadAll(c.Ctx.Request.Body)
|
||
if err := json.Unmarshal(raw, &p); err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "参数错误"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
|
||
update := map[string]interface{}{}
|
||
if strings.TrimSpace(p.Name) != "" {
|
||
update["name"] = strings.TrimSpace(p.Name)
|
||
}
|
||
if p.Status != nil {
|
||
update["status"] = *p.Status
|
||
}
|
||
if p.Rights != nil {
|
||
update["rights"] = sanitizeTenantRights(p.Rights)
|
||
}
|
||
if len(update) == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "无更新字段"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
|
||
// 全局角色(tenant_id=0)由平台端统一维护,租户端只读
|
||
var target models.AdminRole
|
||
if e := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("id", id).
|
||
Filter("cid", 2).
|
||
One(&target); e == nil && target.TenantID == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "全局角色由平台端统一维护,租户端不能修改"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
|
||
// 改名时不能与可见角色重名(含平台默认的全局角色,排除自身)
|
||
if newName, ok := update["name"]; ok {
|
||
dup, derr := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("cid", 2).
|
||
Filter("tenant_id__in", []uint64{0, tid}).
|
||
Filter("name", newName).
|
||
Exclude("id", id).
|
||
Count()
|
||
if derr != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
if dup > 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "已存在同名角色(含平台默认角色),请更换名称"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
}
|
||
|
||
// 仅允许更新本租户名下的角色,防止越权改到其他租户
|
||
cnt, err := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("id", id).
|
||
Filter("cid", 2).
|
||
Filter("tenant_id", tid).
|
||
Update(update)
|
||
if err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "更新失败"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
if cnt == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||
_ = c.ServeJSON()
|
||
}
|
||
|
||
// DeleteRole 删除当前租户的角色
|
||
// DELETE /backend/roles/:id
|
||
func (c *BackendRoleController) DeleteRole() {
|
||
tid, ok := c.currentTenantID()
|
||
if !ok {
|
||
return
|
||
}
|
||
idStr := c.Ctx.Input.Param(":id")
|
||
id, _ := strconv.ParseUint(idStr, 10, 64)
|
||
if id == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 400, "msg": "id 不能为空"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
// 全局角色(tenant_id=0)由平台端统一维护,租户端不能删除
|
||
var target models.AdminRole
|
||
if e := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("id", id).
|
||
Filter("cid", 2).
|
||
One(&target); e == nil && target.TenantID == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 403, "msg": "全局角色由平台端统一维护,租户端不能删除"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
|
||
// 仅允许删除本租户名下的角色
|
||
cnt, err := models.Orm.QueryTable(new(models.AdminRole)).
|
||
Filter("id", id).
|
||
Filter("cid", 2).
|
||
Filter("tenant_id", tid).
|
||
Delete()
|
||
if err != nil {
|
||
c.Data["json"] = map[string]interface{}{"code": 500, "msg": "删除失败"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
if cnt == 0 {
|
||
c.Data["json"] = map[string]interface{}{"code": 404, "msg": "角色不存在"}
|
||
_ = c.ServeJSON()
|
||
return
|
||
}
|
||
c.Data["json"] = map[string]interface{}{"code": 200, "msg": "success"}
|
||
_ = c.ServeJSON()
|
||
}
|