Files
kefu/im/backend/internal/app/admin_rbac.go
T
2026-09-03 08:38:17 +08:00

526 lines
19 KiB
Go

package app
import (
"context"
"database/sql"
"net/http"
"regexp"
"sort"
"strings"
"time"
)
type adminRoleView struct {
ID int64 `json:"id"`
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
BuiltIn bool `json:"builtIn"`
Status int `json:"status"`
Permissions []string `json:"permissions"`
}
var adminAccountPattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_.-]{2,49}$`)
var adminRoleCodePattern = regexp.MustCompile(`^[a-z][a-z0-9_]{2,49}$`)
func (a *App) adminAccess(ctx context.Context, adminID int64) ([]string, []string, error) {
roleRows, err := a.db.QueryContext(ctx, `SELECT DISTINCT r.role_code FROM admin_user_roles ur JOIN admin_roles r ON r.id=ur.role_id WHERE ur.admin_user_id=? AND r.status=1 ORDER BY r.role_code`, adminID)
if err != nil {
return nil, nil, err
}
roles := []string{}
for roleRows.Next() {
var code string
if err = roleRows.Scan(&code); err != nil {
_ = roleRows.Close()
return nil, nil, err
}
roles = append(roles, code)
}
if err = roleRows.Close(); err != nil {
return nil, nil, err
}
permissionRows, err := a.db.QueryContext(ctx, `SELECT DISTINCT rp.permission_code
FROM admin_user_roles ur
JOIN admin_roles r ON r.id=ur.role_id AND r.status=1
JOIN admin_role_permissions rp ON rp.role_id=r.id
WHERE ur.admin_user_id=? ORDER BY rp.permission_code`, adminID)
if err != nil {
return nil, nil, err
}
permissions := []string{}
for permissionRows.Next() {
var code string
if err = permissionRows.Scan(&code); err != nil {
_ = permissionRows.Close()
return nil, nil, err
}
permissions = append(permissions, code)
}
err = permissionRows.Close()
return roles, permissions, err
}
func (a *App) hasAdminPermission(ctx context.Context, adminID int64, permission string) bool {
var allowed int
err := a.db.QueryRowContext(ctx, `SELECT EXISTS(
SELECT 1 FROM admin_user_roles ur
JOIN admin_roles r ON r.id=ur.role_id AND r.status=1
JOIN admin_role_permissions rp ON rp.role_id=r.id
WHERE ur.admin_user_id=? AND rp.permission_code=?)`, adminID, permission).Scan(&allowed)
return err == nil && allowed == 1
}
func (a *App) requireAdminPermission(permission string, next http.HandlerFunc) http.HandlerFunc {
return a.requireAuth("admin", func(w http.ResponseWriter, r *http.Request) {
if !a.hasAdminPermission(r.Context(), current(r).ID, permission) {
fail(w, http.StatusForbidden, 10006, "没有执行该操作的权限")
return
}
next(w, r)
})
}
func (a *App) adminCodes(w http.ResponseWriter, r *http.Request) {
_, permissions, err := a.adminAccess(r.Context(), current(r).ID)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "读取权限失败")
return
}
reply(w, permissions)
}
func (a *App) adminRoleIDs(ctx context.Context, adminID int64) []int64 {
rows, err := a.db.QueryContext(ctx, `SELECT role_id FROM admin_user_roles WHERE admin_user_id=? ORDER BY role_id`, adminID)
if err != nil {
return []int64{}
}
defer rows.Close()
result := []int64{}
for rows.Next() {
var id int64
if rows.Scan(&id) == nil {
result = append(result, id)
}
}
return result
}
func (a *App) adminAccounts(w http.ResponseWriter, r *http.Request) {
page, size, offset := pagination(r)
keyword := strings.TrimSpace(r.URL.Query().Get("keyword"))
where := ""
args := []any{}
if keyword != "" {
where = ` WHERE username LIKE ? OR real_name LIKE ?`
like := "%" + keyword + "%"
args = append(args, like, like)
}
var total int64
if err := a.db.QueryRowContext(r.Context(), `SELECT COUNT(*) FROM admin_users`+where, args...).Scan(&total); err != nil {
fail(w, http.StatusInternalServerError, 50001, "查询管理员失败")
return
}
queryArgs := append(append([]any{}, args...), size, offset)
rows, err := a.db.QueryContext(r.Context(), `SELECT id,username,real_name,avatar_url,status,last_login_at,password_changed_at,created_at FROM admin_users`+where+` ORDER BY id LIMIT ? OFFSET ?`, queryArgs...)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "查询管理员失败")
return
}
defer rows.Close()
items := []map[string]any{}
for rows.Next() {
var id int64
var username, realName, avatar string
var status int
var lastLogin, passwordChanged sql.NullTime
var createdAt time.Time
if err = rows.Scan(&id, &username, &realName, &avatar, &status, &lastLogin, &passwordChanged, &createdAt); err != nil {
fail(w, http.StatusInternalServerError, 50001, "读取管理员失败")
return
}
roles, _, _ := a.adminAccess(r.Context(), id)
items = append(items, map[string]any{
"id": id, "username": username, "realName": realName, "avatar": avatar,
"status": status, "roles": roles, "roleIds": a.adminRoleIDs(r.Context(), id),
"lastLoginAt": nullableTime(lastLogin), "passwordChangedAt": nullableTime(passwordChanged), "createdAt": createdAt,
})
}
reply(w, pageResult{Items: items, Total: total, Page: page, Size: size})
}
func validateAdminRoleIDs(ctx context.Context, tx *sql.Tx, roleIDs []int64) error {
if len(roleIDs) == 0 {
return sql.ErrNoRows
}
seen := map[int64]bool{}
for _, roleID := range roleIDs {
if roleID <= 0 || seen[roleID] {
return sql.ErrNoRows
}
seen[roleID] = true
var status int
if err := tx.QueryRowContext(ctx, `SELECT status FROM admin_roles WHERE id=?`, roleID).Scan(&status); err != nil || status != 1 {
return sql.ErrNoRows
}
}
return nil
}
func replaceAdminRoles(ctx context.Context, tx *sql.Tx, adminID int64, roleIDs []int64) error {
if err := validateAdminRoleIDs(ctx, tx, roleIDs); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM admin_user_roles WHERE admin_user_id=?`, adminID); err != nil {
return err
}
for _, roleID := range roleIDs {
if _, err := tx.ExecContext(ctx, `INSERT INTO admin_user_roles(admin_user_id,role_id) VALUES(?,?)`, adminID, roleID); err != nil {
return err
}
}
return nil
}
func (a *App) adminCreateAccount(w http.ResponseWriter, r *http.Request) {
var req struct {
Username string `json:"username"`
RealName string `json:"realName"`
Password string `json:"password"`
Avatar string `json:"avatar"`
RoleIDs []int64 `json:"roleIds"`
}
if decode(r, &req) != nil {
fail(w, http.StatusBadRequest, 20001, "管理员资料格式错误")
return
}
req.Username = strings.TrimSpace(req.Username)
req.RealName = strings.TrimSpace(req.RealName)
if !adminAccountPattern.MatchString(req.Username) || req.RealName == "" || len([]rune(req.RealName)) > 50 {
fail(w, http.StatusBadRequest, 20001, "账号或姓名格式无效")
return
}
if req.Password == "" {
fail(w, http.StatusBadRequest, 20001, "请输入密码")
return
}
hash, err := hashPassword(req.Password)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "密码加密失败")
return
}
tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "创建管理员失败")
return
}
defer func() { _ = tx.Rollback() }()
if err = validateAdminRoleIDs(r.Context(), tx, req.RoleIDs); err != nil {
fail(w, http.StatusBadRequest, 20001, "请至少选择一个有效角色")
return
}
result, err := tx.ExecContext(r.Context(), `INSERT INTO admin_users(username,password_hash,real_name,avatar_url,status,password_changed_at) VALUES(?,?,?,?,1,NOW(3))`, req.Username, hash, req.RealName, strings.TrimSpace(req.Avatar))
if err != nil {
fail(w, http.StatusConflict, 20001, "管理员账号已存在")
return
}
adminID, _ := result.LastInsertId()
if err = replaceAdminRoles(r.Context(), tx, adminID, req.RoleIDs); err != nil || tx.Commit() != nil {
fail(w, http.StatusInternalServerError, 50001, "保存管理员角色失败")
return
}
a.audit(r, "create", "admin_user", adminID, map[string]any{"username": req.Username, "roleIds": req.RoleIDs})
reply(w, map[string]any{"id": adminID, "success": true})
}
func containsInt64(values []int64, expected int64) bool {
for _, value := range values {
if value == expected {
return true
}
}
return false
}
func (a *App) ensureSuperAdminRemains(ctx context.Context, tx *sql.Tx, targetID int64, newStatus int, roleIDs []int64) error {
var superRoleID int64
if err := tx.QueryRowContext(ctx, `SELECT id FROM admin_roles WHERE role_code='super_admin'`).Scan(&superRoleID); err != nil {
return err
}
var targetWasSuper int
if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM admin_user_roles WHERE admin_user_id=? AND role_id=?)`, targetID, superRoleID).Scan(&targetWasSuper); err != nil {
return err
}
if targetWasSuper == 0 || (newStatus == 1 && containsInt64(roleIDs, superRoleID)) {
return nil
}
var remaining int
if err := tx.QueryRowContext(ctx, `SELECT COUNT(DISTINCT a.id) FROM admin_users a JOIN admin_user_roles ur ON ur.admin_user_id=a.id WHERE a.status=1 AND a.id<>? AND ur.role_id=?`, targetID, superRoleID).Scan(&remaining); err != nil {
return err
}
if remaining == 0 {
return sql.ErrNoRows
}
return nil
}
func (a *App) adminUpdateAccount(w http.ResponseWriter, r *http.Request) {
adminID, err := pathID(r)
if err != nil {
fail(w, http.StatusBadRequest, 20001, "管理员 ID 无效")
return
}
var req struct {
RealName string `json:"realName"`
Avatar string `json:"avatar"`
Password string `json:"password"`
Status int `json:"status"`
RoleIDs []int64 `json:"roleIds"`
}
if decode(r, &req) != nil || strings.TrimSpace(req.RealName) == "" || (req.Status != 0 && req.Status != 1) {
fail(w, http.StatusBadRequest, 20001, "管理员资料格式错误")
return
}
if current(r).ID == adminID && req.Status != 1 {
fail(w, http.StatusBadRequest, 20001, "不能停用当前登录账号")
return
}
tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "更新管理员失败")
return
}
defer func() { _ = tx.Rollback() }()
var exists int64
if err = tx.QueryRowContext(r.Context(), `SELECT id FROM admin_users WHERE id=? FOR UPDATE`, adminID).Scan(&exists); err != nil {
fail(w, http.StatusNotFound, 30001, "管理员不存在")
return
}
if err = validateAdminRoleIDs(r.Context(), tx, req.RoleIDs); err != nil {
fail(w, http.StatusBadRequest, 20001, "请至少选择一个有效角色")
return
}
if err = a.ensureSuperAdminRemains(r.Context(), tx, adminID, req.Status, req.RoleIDs); err != nil {
fail(w, http.StatusBadRequest, 20001, "必须保留至少一个启用的超级管理员")
return
}
if _, err = tx.ExecContext(r.Context(), `UPDATE admin_users SET real_name=?,avatar_url=?,status=? WHERE id=?`, strings.TrimSpace(req.RealName), strings.TrimSpace(req.Avatar), req.Status, adminID); err != nil {
fail(w, http.StatusInternalServerError, 50001, "更新管理员失败")
return
}
if err = replaceAdminRoles(r.Context(), tx, adminID, req.RoleIDs); err != nil {
fail(w, http.StatusInternalServerError, 50001, "更新管理员角色失败")
return
}
passwordChanged := false
if req.Password != "" {
hash, hashErr := hashPassword(req.Password)
if hashErr != nil {
fail(w, http.StatusInternalServerError, 50001, "密码加密失败")
return
}
if _, err = tx.ExecContext(r.Context(), `UPDATE admin_users SET password_hash=?,token_version=token_version+1,password_changed_at=NOW(3) WHERE id=?`, hash, adminID); err != nil {
fail(w, http.StatusInternalServerError, 50001, "更新管理员密码失败")
return
}
passwordChanged = true
}
if req.Status != 1 || passwordChanged {
if _, err = tx.ExecContext(r.Context(), `UPDATE admin_sessions SET revoked_at=NOW(3) WHERE admin_user_id=? AND revoked_at IS NULL`, adminID); err != nil {
fail(w, http.StatusInternalServerError, 50001, "撤销管理员会话失败")
return
}
}
if err = tx.Commit(); err != nil {
fail(w, http.StatusInternalServerError, 50001, "更新管理员失败")
return
}
a.audit(r, "update", "admin_user", adminID, map[string]any{"roleIds": req.RoleIDs, "status": req.Status, "passwordChanged": passwordChanged})
reply(w, map[string]bool{"success": true})
}
func (a *App) rolePermissions(ctx context.Context, roleID int64) []string {
rows, err := a.db.QueryContext(ctx, `SELECT permission_code FROM admin_role_permissions WHERE role_id=? ORDER BY permission_code`, roleID)
if err != nil {
return []string{}
}
defer rows.Close()
permissions := []string{}
for rows.Next() {
var code string
if rows.Scan(&code) == nil {
permissions = append(permissions, code)
}
}
return permissions
}
func (a *App) adminRoles(w http.ResponseWriter, r *http.Request) {
rows, err := a.db.QueryContext(r.Context(), `SELECT id,role_code,role_name,description,built_in,status FROM admin_roles ORDER BY built_in DESC,id`)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "查询角色失败")
return
}
defer rows.Close()
items := []adminRoleView{}
for rows.Next() {
var item adminRoleView
var builtIn int
if err = rows.Scan(&item.ID, &item.Code, &item.Name, &item.Description, &builtIn, &item.Status); err != nil {
fail(w, http.StatusInternalServerError, 50001, "读取角色失败")
return
}
item.BuiltIn = builtIn == 1
item.Permissions = a.rolePermissions(r.Context(), item.ID)
items = append(items, item)
}
reply(w, map[string]any{"items": items})
}
func (a *App) adminPermissions(w http.ResponseWriter, r *http.Request) {
rows, err := a.db.QueryContext(r.Context(), `SELECT permission_code,permission_name,permission_group FROM admin_permissions ORDER BY permission_group,sort_order,permission_code`)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "查询权限失败")
return
}
defer rows.Close()
items := []map[string]string{}
for rows.Next() {
var code, name, group string
if rows.Scan(&code, &name, &group) == nil {
items = append(items, map[string]string{"code": code, "name": name, "group": group})
}
}
reply(w, map[string]any{"items": items})
}
func validatePermissionCodes(ctx context.Context, tx *sql.Tx, permissions []string) error {
if len(permissions) == 0 {
return sql.ErrNoRows
}
seen := map[string]bool{}
for _, permission := range permissions {
permission = strings.TrimSpace(permission)
if permission == "" || seen[permission] {
return sql.ErrNoRows
}
seen[permission] = true
var exists int
if err := tx.QueryRowContext(ctx, `SELECT EXISTS(SELECT 1 FROM admin_permissions WHERE permission_code=?)`, permission).Scan(&exists); err != nil || exists != 1 {
return sql.ErrNoRows
}
}
return nil
}
func replaceRolePermissions(ctx context.Context, tx *sql.Tx, roleID int64, permissions []string) error {
if err := validatePermissionCodes(ctx, tx, permissions); err != nil {
return err
}
if _, err := tx.ExecContext(ctx, `DELETE FROM admin_role_permissions WHERE role_id=?`, roleID); err != nil {
return err
}
sort.Strings(permissions)
for _, permission := range permissions {
if _, err := tx.ExecContext(ctx, `INSERT INTO admin_role_permissions(role_id,permission_code) VALUES(?,?)`, roleID, permission); err != nil {
return err
}
}
return nil
}
func (a *App) adminCreateRole(w http.ResponseWriter, r *http.Request) {
a.adminSaveRole(w, r, 0)
}
func (a *App) adminUpdateRole(w http.ResponseWriter, r *http.Request) {
roleID, err := pathID(r)
if err != nil {
fail(w, http.StatusBadRequest, 20001, "角色 ID 无效")
return
}
a.adminSaveRole(w, r, roleID)
}
func (a *App) adminSaveRole(w http.ResponseWriter, r *http.Request, roleID int64) {
var req struct {
Code string `json:"code"`
Name string `json:"name"`
Description string `json:"description"`
Permissions []string `json:"permissions"`
Status int `json:"status"`
}
if decode(r, &req) != nil {
fail(w, http.StatusBadRequest, 20001, "角色资料格式错误")
return
}
req.Code = strings.TrimSpace(req.Code)
req.Name = strings.TrimSpace(req.Name)
if !adminRoleCodePattern.MatchString(req.Code) || req.Name == "" || len([]rune(req.Name)) > 80 || (req.Status != 0 && req.Status != 1) {
fail(w, http.StatusBadRequest, 20001, "角色编码、名称或状态无效")
return
}
tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted})
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "保存角色失败")
return
}
defer func() { _ = tx.Rollback() }()
if err = validatePermissionCodes(r.Context(), tx, req.Permissions); err != nil {
fail(w, http.StatusBadRequest, 20001, "请至少选择一个有效权限")
return
}
if roleID == 0 {
result, insertErr := tx.ExecContext(r.Context(), `INSERT INTO admin_roles(role_code,role_name,description,status) VALUES(?,?,?,?)`, req.Code, req.Name, strings.TrimSpace(req.Description), req.Status)
if insertErr != nil {
fail(w, http.StatusConflict, 20001, "角色编码已存在")
return
}
roleID, _ = result.LastInsertId()
} else {
var builtIn int
if err = tx.QueryRowContext(r.Context(), `SELECT built_in FROM admin_roles WHERE id=? FOR UPDATE`, roleID).Scan(&builtIn); err != nil {
fail(w, http.StatusNotFound, 30001, "角色不存在")
return
}
if builtIn == 1 {
fail(w, http.StatusBadRequest, 20001, "内置超级管理员角色不可修改")
return
}
if _, err = tx.ExecContext(r.Context(), `UPDATE admin_roles SET role_code=?,role_name=?,description=?,status=? WHERE id=?`, req.Code, req.Name, strings.TrimSpace(req.Description), req.Status, roleID); err != nil {
fail(w, http.StatusConflict, 20001, "角色编码已存在")
return
}
}
if err = replaceRolePermissions(r.Context(), tx, roleID, req.Permissions); err != nil || tx.Commit() != nil {
fail(w, http.StatusInternalServerError, 50001, "保存角色权限失败")
return
}
a.audit(r, "save", "admin_role", roleID, map[string]any{"code": req.Code, "permissions": req.Permissions, "status": req.Status})
reply(w, map[string]any{"id": roleID, "success": true})
}
func (a *App) adminDeleteRole(w http.ResponseWriter, r *http.Request) {
roleID, err := pathID(r)
if err != nil {
fail(w, http.StatusBadRequest, 20001, "角色 ID 无效")
return
}
var builtIn, assigned int
if err = a.db.QueryRowContext(r.Context(), `SELECT built_in,(SELECT COUNT(*) FROM admin_user_roles WHERE role_id=admin_roles.id) FROM admin_roles WHERE id=?`, roleID).Scan(&builtIn, &assigned); err != nil {
fail(w, http.StatusNotFound, 30001, "角色不存在")
return
}
if builtIn == 1 || assigned > 0 {
fail(w, http.StatusBadRequest, 20001, "内置角色或仍被管理员使用的角色不能删除")
return
}
if _, err = a.db.ExecContext(r.Context(), `DELETE FROM admin_roles WHERE id=?`, roleID); err != nil {
fail(w, http.StatusInternalServerError, 50001, "删除角色失败")
return
}
a.audit(r, "delete", "admin_role", roleID, nil)
reply(w, map[string]bool{"success": true})
}