后端: - 种子数据:超级管理员(admin/admin123)、super_admin 角色、22 条菜单(含按钮权限码)、角色绑定 - admin_menu 扩展 icon/component/hidden 字段(003_schema_ext.sql),手动补齐 entity/do/table - auth 扩展:/auth/info、/auth/codes(仅登录),路由拆分公开/Profile/受保护三组 - 菜单路由:/menu/routes 返回 vben backend 动态路由树(按角色过滤、排序) - RBAC 管理:/admins、/roles、/menus CRUD(含角色绑定、重置密码、菜单授权) - 日志监控:/log/files、/log/tail(尾部读取+关键词过滤+路径穿越防护) - 安全加固:接口鉴权改为「方法+路径→权限码」自动映射,杜绝任意权限码越权 - 修复:gf v2.10.2 不自动替换→cmd 注入;MySQL driver 需显式引入 contrib - 全链路冒烟测试通过(含越权拒绝 30003)
384 lines
13 KiB
Go
384 lines
13 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|
"golang.org/x/crypto/bcrypt"
|
|
|
|
"service.xpcool.com/internal/consts"
|
|
"service.xpcool.com/internal/dao"
|
|
"service.xpcool.com/internal/library/response"
|
|
"service.xpcool.com/internal/model/do"
|
|
"service.xpcool.com/internal/model/dto"
|
|
"service.xpcool.com/internal/model/entity"
|
|
)
|
|
|
|
// IAdminManage manages administrators.
|
|
type IAdminManage interface {
|
|
List(context.Context, dto.PageQuery) ([]*dto.AdminItem, int, error)
|
|
Create(context.Context, dto.AdminCreateInput) (uint64, error)
|
|
Update(context.Context, dto.AdminUpdateInput) error
|
|
ResetPassword(context.Context, uint64, string) error
|
|
Delete(context.Context, uint64) error
|
|
}
|
|
|
|
// IRoleManage manages roles and their menu bindings.
|
|
type IRoleManage interface {
|
|
List(context.Context, dto.PageQuery) ([]*dto.RoleItem, int, error)
|
|
Create(context.Context, dto.RoleCreateInput) (uint64, error)
|
|
Update(context.Context, dto.RoleUpdateInput) error
|
|
Delete(context.Context, uint64) error
|
|
}
|
|
|
|
// IMenuManage manages the menu tree (menus + button permissions).
|
|
type IMenuManage interface {
|
|
Tree(context.Context) ([]*dto.MenuNode, error)
|
|
Create(context.Context, dto.MenuCreateInput) (uint64, error)
|
|
Update(context.Context, dto.MenuUpdateInput) error
|
|
Delete(context.Context, uint64) error
|
|
}
|
|
|
|
type adminManage struct{}
|
|
type roleManage struct{}
|
|
type menuManage struct{}
|
|
|
|
func NewAdminManage() IAdminManage { return &adminManage{} }
|
|
func NewRoleManage() IRoleManage { return &roleManage{} }
|
|
func NewMenuManage() IMenuManage { return &menuManage{} }
|
|
|
|
var (
|
|
localAdminManage IAdminManage
|
|
localRoleManage IRoleManage
|
|
localMenuManage IMenuManage
|
|
)
|
|
|
|
func AdminManage() IAdminManage {
|
|
if localAdminManage == nil {
|
|
panic("AdminManage implementation not registered")
|
|
}
|
|
return localAdminManage
|
|
}
|
|
func RegisterAdminManage(i IAdminManage) { localAdminManage = i }
|
|
func RoleManage() IRoleManage {
|
|
if localRoleManage == nil {
|
|
panic("RoleManage implementation not registered")
|
|
}
|
|
return localRoleManage
|
|
}
|
|
func RegisterRoleManage(i IRoleManage) { localRoleManage = i }
|
|
func MenuManage() IMenuManage {
|
|
if localMenuManage == nil {
|
|
panic("MenuManage implementation not registered")
|
|
}
|
|
return localMenuManage
|
|
}
|
|
func RegisterMenuManage(i IMenuManage) { localMenuManage = i }
|
|
|
|
// ---------------------- Admin manage ----------------------
|
|
|
|
func (s *adminManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.AdminItem, int, error) {
|
|
total, err := dao.AdminUser.Ctx(ctx).Count()
|
|
if err != nil {
|
|
return nil, 0, gerror.Wrap(err, "count admins")
|
|
}
|
|
if total == 0 {
|
|
return nil, 0, nil
|
|
}
|
|
var list []entity.AdminUser
|
|
if err = dao.AdminUser.Ctx(ctx).Page(q.Page, q.Size).OrderDesc("id").Scan(&list); err != nil {
|
|
return nil, 0, gerror.Wrap(err, "query admins")
|
|
}
|
|
items := make([]*dto.AdminItem, 0, len(list))
|
|
for i := range list {
|
|
a := &list[i]
|
|
roleIds, roleNames, err := s.roles(ctx, a.Id)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, &dto.AdminItem{
|
|
Id: a.Id, Username: a.Username, Nickname: a.Nickname, Status: a.Status,
|
|
RoleIds: roleIds, RoleNames: roleNames,
|
|
CreatedAt: a.CreatedAt.Layout("2006-01-02 15:04:05"),
|
|
})
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func (s *adminManage) roles(ctx context.Context, adminID uint64) ([]uint64, []string, error) {
|
|
var rels []entity.AdminUserRole
|
|
if err := dao.AdminUserRole.Ctx(ctx).Where(do.AdminUserRole{AdminUserId: adminID}).Scan(&rels); err != nil {
|
|
return nil, nil, gerror.Wrap(err, "query admin roles")
|
|
}
|
|
roleIds := make([]uint64, 0, len(rels))
|
|
for _, r := range rels {
|
|
roleIds = append(roleIds, r.RoleId)
|
|
}
|
|
if len(roleIds) == 0 {
|
|
return roleIds, nil, nil
|
|
}
|
|
var roles []entity.AdminRole
|
|
if err := dao.AdminRole.Ctx(ctx).WhereIn("id", roleIds).Scan(&roles); err != nil {
|
|
return nil, nil, gerror.Wrap(err, "query roles")
|
|
}
|
|
names := make([]string, 0, len(roles))
|
|
for _, r := range roles {
|
|
names = append(names, r.Name)
|
|
}
|
|
return roleIds, names, nil
|
|
}
|
|
|
|
func (s *adminManage) Create(ctx context.Context, in dto.AdminCreateInput) (uint64, error) {
|
|
count, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Username: in.Username}).Count()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "check username")
|
|
}
|
|
if count > 0 {
|
|
return 0, response.Error(consts.CodeInvalidParam, "username already exists")
|
|
}
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(in.Password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "hash password")
|
|
}
|
|
id, err := dao.AdminUser.Ctx(ctx).Data(do.AdminUser{
|
|
Username: in.Username, PasswordHash: string(hash), Nickname: in.Nickname, Status: 1,
|
|
}).InsertAndGetId()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "insert admin")
|
|
}
|
|
if err = bindAdminRoles(ctx, uint64(id), in.RoleIds); err != nil {
|
|
return 0, err
|
|
}
|
|
return uint64(id), nil
|
|
}
|
|
|
|
func (s *adminManage) Update(ctx context.Context, in dto.AdminUpdateInput) error {
|
|
data := do.AdminUser{Nickname: in.Nickname, Status: in.Status}
|
|
if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: in.Id}).Data(data).Update(); err != nil {
|
|
return gerror.Wrap(err, "update admin")
|
|
}
|
|
return bindAdminRoles(ctx, in.Id, in.RoleIds)
|
|
}
|
|
|
|
func (s *adminManage) ResetPassword(ctx context.Context, id uint64, password string) error {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return gerror.Wrap(err, "hash password")
|
|
}
|
|
if _, err = dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Data(do.AdminUser{PasswordHash: string(hash)}).Update(); err != nil {
|
|
return gerror.Wrap(err, "reset password")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *adminManage) Delete(ctx context.Context, id uint64) error {
|
|
if _, err := dao.AdminUser.Ctx(ctx).Where(do.AdminUser{Id: id}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "delete admin")
|
|
}
|
|
// 关联关系物理删除,避免唯一键残留。
|
|
if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: id}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "delete admin role bindings")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// bindAdminRoles 全量重绑管理员-角色关系。
|
|
func bindAdminRoles(ctx context.Context, adminID uint64, roleIds []uint64) error {
|
|
if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{AdminUserId: adminID}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "clear admin roles")
|
|
}
|
|
for _, rid := range roleIds {
|
|
if _, err := dao.AdminUserRole.Ctx(ctx).Data(do.AdminUserRole{AdminUserId: adminID, RoleId: rid}).Insert(); err != nil {
|
|
return gerror.Wrap(err, "bind admin role")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------------------- Role manage ----------------------
|
|
|
|
func (s *roleManage) List(ctx context.Context, q dto.PageQuery) ([]*dto.RoleItem, int, error) {
|
|
total, err := dao.AdminRole.Ctx(ctx).Count()
|
|
if err != nil {
|
|
return nil, 0, gerror.Wrap(err, "count roles")
|
|
}
|
|
if total == 0 {
|
|
return nil, 0, nil
|
|
}
|
|
var list []entity.AdminRole
|
|
if err = dao.AdminRole.Ctx(ctx).Page(q.Page, q.Size).OrderAsc("id").Scan(&list); err != nil {
|
|
return nil, 0, gerror.Wrap(err, "query roles")
|
|
}
|
|
items := make([]*dto.RoleItem, 0, len(list))
|
|
for i := range list {
|
|
r := &list[i]
|
|
menuIds, err := roleMenuIDs(ctx, r.Id)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, &dto.RoleItem{
|
|
Id: r.Id, Code: r.Code, Name: r.Name, Status: r.Status, MenuIds: menuIds,
|
|
CreatedAt: r.CreatedAt.Layout("2006-01-02 15:04:05"),
|
|
})
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func (s *roleManage) Create(ctx context.Context, in dto.RoleCreateInput) (uint64, error) {
|
|
count, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Code: in.Code}).Count()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "check role code")
|
|
}
|
|
if count > 0 {
|
|
return 0, response.Error(consts.CodeInvalidParam, "role code already exists")
|
|
}
|
|
id, err := dao.AdminRole.Ctx(ctx).Data(do.AdminRole{Code: in.Code, Name: in.Name, Status: in.Status}).InsertAndGetId()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "insert role")
|
|
}
|
|
if err = bindRoleMenus(ctx, uint64(id), in.MenuIds); err != nil {
|
|
return 0, err
|
|
}
|
|
return uint64(id), nil
|
|
}
|
|
|
|
func (s *roleManage) Update(ctx context.Context, in dto.RoleUpdateInput) error {
|
|
if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: in.Id}).Data(do.AdminRole{Name: in.Name, Status: in.Status}).Update(); err != nil {
|
|
return gerror.Wrap(err, "update role")
|
|
}
|
|
return bindRoleMenus(ctx, in.Id, in.MenuIds)
|
|
}
|
|
|
|
func (s *roleManage) Delete(ctx context.Context, id uint64) error {
|
|
if _, err := dao.AdminRole.Ctx(ctx).Where(do.AdminRole{Id: id}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "delete role")
|
|
}
|
|
if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: id}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "delete role menu bindings")
|
|
}
|
|
if _, err := dao.AdminUserRole.Ctx(ctx).Unscoped().Where(do.AdminUserRole{RoleId: id}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "delete user role bindings")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func roleMenuIDs(ctx context.Context, roleID uint64) ([]uint64, error) {
|
|
var rels []entity.AdminRoleMenu
|
|
if err := dao.AdminRoleMenu.Ctx(ctx).Where(do.AdminRoleMenu{RoleId: roleID}).Scan(&rels); err != nil {
|
|
return nil, gerror.Wrap(err, "query role menus")
|
|
}
|
|
ids := make([]uint64, 0, len(rels))
|
|
for _, r := range rels {
|
|
ids = append(ids, r.MenuId)
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
// bindRoleMenus 全量重绑角色-菜单关系。
|
|
func bindRoleMenus(ctx context.Context, roleID uint64, menuIds []uint64) error {
|
|
if _, err := dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{RoleId: roleID}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "clear role menus")
|
|
}
|
|
for _, mid := range menuIds {
|
|
if _, err := dao.AdminRoleMenu.Ctx(ctx).Data(do.AdminRoleMenu{RoleId: roleID, MenuId: mid}).Insert(); err != nil {
|
|
return gerror.Wrap(err, "bind role menu")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ---------------------- Menu manage ----------------------
|
|
|
|
func (s *menuManage) Tree(ctx context.Context) ([]*dto.MenuNode, error) {
|
|
var list []entity.AdminMenu
|
|
if err := dao.AdminMenu.Ctx(ctx).OrderAsc("sort").Scan(&list); err != nil {
|
|
return nil, gerror.Wrap(err, "query menus")
|
|
}
|
|
byID := make(map[uint64]*dto.MenuNode, len(list))
|
|
for i := range list {
|
|
m := &list[i]
|
|
byID[m.Id] = &dto.MenuNode{
|
|
Id: m.Id, ParentId: m.ParentId, Name: m.Name, Icon: m.Icon, Type: m.Type,
|
|
Path: m.Path, Component: m.Component, Permission: m.Permission,
|
|
Sort: m.Sort, Status: m.Status, Hidden: m.Hidden,
|
|
}
|
|
}
|
|
var roots []*dto.MenuNode
|
|
for i := range list {
|
|
m := &list[i]
|
|
node := byID[m.Id]
|
|
if m.ParentId == 0 {
|
|
roots = append(roots, node)
|
|
} else if p, ok := byID[m.ParentId]; ok {
|
|
p.Children = append(p.Children, node)
|
|
}
|
|
}
|
|
sort.Slice(roots, func(i, j int) bool { return roots[i].Sort < roots[j].Sort })
|
|
for _, r := range roots {
|
|
sortMenuChildren(r)
|
|
}
|
|
return roots, nil
|
|
}
|
|
|
|
func sortMenuChildren(n *dto.MenuNode) {
|
|
sort.Slice(n.Children, func(i, j int) bool { return n.Children[i].Sort < n.Children[j].Sort })
|
|
for _, c := range n.Children {
|
|
sortMenuChildren(c)
|
|
}
|
|
}
|
|
|
|
func (s *menuManage) Create(ctx context.Context, in dto.MenuCreateInput) (uint64, error) {
|
|
count, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Permission: in.Permission}).Count()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "check menu permission")
|
|
}
|
|
if count > 0 {
|
|
return 0, response.Error(consts.CodeInvalidParam, "permission already exists")
|
|
}
|
|
id, err := dao.AdminMenu.Ctx(ctx).Data(do.AdminMenu{
|
|
ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path,
|
|
Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden,
|
|
}).InsertAndGetId()
|
|
if err != nil {
|
|
return 0, gerror.Wrap(err, "insert menu")
|
|
}
|
|
return uint64(id), nil
|
|
}
|
|
|
|
func (s *menuManage) Update(ctx context.Context, in dto.MenuUpdateInput) error {
|
|
count, err := dao.AdminMenu.Ctx(ctx).Where("permission = ? AND id != ?", in.Permission, in.Id).Count()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "check menu permission")
|
|
}
|
|
if count > 0 {
|
|
return response.Error(consts.CodeInvalidParam, "permission already exists")
|
|
}
|
|
_, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: in.Id}).Data(do.AdminMenu{
|
|
ParentId: in.ParentId, Name: in.Name, Icon: in.Icon, Type: in.Type, Path: in.Path,
|
|
Component: in.Component, Permission: in.Permission, Sort: in.Sort, Status: in.Status, Hidden: in.Hidden,
|
|
}).Update()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "update menu")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *menuManage) Delete(ctx context.Context, id uint64) error {
|
|
// 删除前检查是否存在子节点。
|
|
cnt, err := dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{ParentId: id}).Count()
|
|
if err != nil {
|
|
return gerror.Wrap(err, "check menu children")
|
|
}
|
|
if cnt > 0 {
|
|
return response.Error(consts.CodeInvalidParam, "delete children first")
|
|
}
|
|
if _, err = dao.AdminMenu.Ctx(ctx).Where(do.AdminMenu{Id: id}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "delete menu")
|
|
}
|
|
if _, err = dao.AdminRoleMenu.Ctx(ctx).Unscoped().Where(do.AdminRoleMenu{MenuId: id}).Delete(); err != nil {
|
|
return gerror.Wrap(err, "delete role menu bindings")
|
|
}
|
|
return nil
|
|
}
|