Smail/backend/controllers/account_group.go
smail 4cf1de87d3
Some checks are pending
Security Checks / security (push) Waiting to run
修复管理员分组版本校验和受限保存
修复管理员用户列表成员版本缺失导致的多分组保存失败,并保留管理员不可管理分组的成员关系,补充回归测试。
2026-10-02 06:26:18 +08:00

170 lines
5.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package controllers
import (
"errors"
"net/http"
"git.pcux.cn/xuans/Smail/db"
"git.pcux.cn/xuans/Smail/dto/response"
"git.pcux.cn/xuans/Smail/models"
"git.pcux.cn/xuans/Smail/services/access"
"git.pcux.cn/xuans/Smail/services/accountgroup"
"git.pcux.cn/xuans/Smail/utils/context"
)
func accountWriteGuard(ctx *context.Context, target models.User) accountgroup.WriteGuard {
if ctx == nil {
return nil
}
return accountgroup.Guard(ctx.UserID, ctx.EffectiveRole(), ctx.SessionVersion, ctx.SecurityVersion, target)
}
func requireAccountScope(ctx *context.Context, w http.ResponseWriter, targetID int) bool {
if ctx == nil || !accountgroup.CanAccess(db.Instance, ctx.UserID, ctx.EffectiveRole(), targetID) {
writeSecurityError(w, response.NoAccessPrivileges, "无权查看或管理该账号")
return false
}
return true
}
func GetAccountGroups(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
if ctx == nil {
writeSecurityError(w, response.NeedLogin, "请先登录")
return
}
groups, err := accountgroup.VisibleGroups(db.Instance, ctx.UserID, ctx.EffectiveRole())
if err != nil {
writeAccountGroupError(w, err)
return
}
w.Header().Set("Cache-Control", "no-store")
response.NewSuccessResponse(map[string]any{"groups": groups, "can_manage": ctx.EffectiveRole() == access.RoleSuperAdmin}).FPrint(w)
}
func requireAccountGroupManagement(ctx *context.Context, w http.ResponseWriter) bool {
if err := authorizeCapability(ctx, access.CapabilityAccountGroupManage, "", false); err != nil {
writeUserAuthorizationError(w, err, "无权管理账号分组")
return false
}
return true
}
func requireAccountGroupMembershipManagement(ctx *context.Context, w http.ResponseWriter) bool {
if err := authorizeCapability(ctx, access.CapabilityAccountGroupMemberManage, "", false); err != nil {
writeUserAuthorizationError(w, err, "无权管理账号所属分组")
return false
}
return true
}
func SaveAccountGroup(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
if !requireAccountGroupManagement(ctx, w) {
return
}
var input struct {
ID int `json:"id"`
Name string `json:"name"`
}
if decodeUserRequest(w, req, &input) != nil || input.ID < 0 {
writeAccountGroupError(w, accountgroup.ErrInvalid)
return
}
name, err := accountgroup.NormalizeName(input.Name)
if err != nil {
writeAccountGroupError(w, err)
return
}
if db.Instance == nil {
writeSecurityError(w, response.ServerError, "数据库不可用")
return
}
item := models.AccountGroup{ID: input.ID, Name: name}
if item.ID == 0 {
_, err = db.Instance.Insert(&item)
} else {
var found bool
found, err = db.Instance.ID(item.ID).Exist(&models.AccountGroup{})
if err == nil && !found {
err = accountgroup.ErrInvalid
}
if err == nil {
_, err = db.Instance.ID(item.ID).Cols("name").Update(&item)
}
}
if err != nil {
writeAccountGroupError(w, err)
return
}
response.NewSuccessResponse(item).FPrint(w)
}
func SetAccountGroupMembership(ctx *context.Context, w http.ResponseWriter, req *http.Request) {
if !requireAccountGroupMembershipManagement(ctx, w) {
return
}
var input struct {
UserID int `json:"user_id"`
GroupIDs []int `json:"group_ids"`
Version int `json:"version"`
}
if decodeUserRequest(w, req, &input) != nil || input.UserID <= 0 || input.Version < 1 || input.GroupIDs == nil {
writeAccountGroupError(w, accountgroup.ErrInvalid)
return
}
if db.Instance == nil {
writeSecurityError(w, response.ServerError, "数据库不可用")
return
}
var user models.User
found, err := db.Instance.ID(input.UserID).Get(&user)
if err != nil || !found {
writeAccountGroupError(w, accountgroup.ErrInvalid)
return
}
if input.Version != user.SecurityVersion {
writeAccountGroupError(w, accountgroup.ErrConflict)
return
}
guards := []accountgroup.WriteGuard{
accountWriteGuard(ctx, user),
accountgroup.MembershipScopeGuard(ctx.UserID, ctx.EffectiveRole(), input.GroupIDs),
}
var setErr error
if ctx.EffectiveRole() == access.RoleAdmin {
visibleGroups, visibleErr := accountgroup.VisibleGroups(db.Instance, ctx.UserID, ctx.EffectiveRole())
if visibleErr != nil {
writeAccountGroupError(w, visibleErr)
return
}
editableGroupIDs := make([]int, 0, len(visibleGroups))
for _, group := range visibleGroups {
editableGroupIDs = append(editableGroupIDs, group.ID)
}
setErr = accountgroup.SetScopedMembership(db.Instance, user, input.GroupIDs, editableGroupIDs, guards...)
} else {
setErr = accountgroup.SetMembership(db.Instance, user, input.GroupIDs, guards...)
}
if setErr != nil {
writeAccountGroupError(w, setErr)
return
}
InvalidateSecurityRuntimeUser(user.ID, user.Account)
response.NewSuccessResponse(map[string]any{
"user_id": user.ID,
"membership_version": user.SecurityVersion + 1,
}).FPrint(w)
}
func writeAccountGroupError(w http.ResponseWriter, err error) {
switch {
case errors.Is(err, accountgroup.ErrDenied):
writeSecurityError(w, response.NoAccessPrivileges, "无权查看或管理该分组")
case errors.Is(err, accountgroup.ErrInvalid):
writeSecurityError(w, response.ParamsError, "分组参数无效:名称为 1–32 个字符,普通用户只能属于一个分组")
case errors.Is(err, accountgroup.ErrConflict):
writeSecurityError(w, response.ParamsError, "账号状态已变化,请刷新列表后重试")
default:
writeSecurityError(w, response.ServerError, "分组操作失败,请检查是否重名并刷新后重试")
}
}