Scert/backend/internal/httpapi/users_api.go

312 lines
9.7 KiB
Go

package httpapi
import (
"errors"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"golang.org/x/crypto/bcrypt"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"git.pcux.cn/xuans/Scert/backend/internal/passwordpolicy"
"git.pcux.cn/xuans/Scert/backend/internal/repository"
)
type userRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
Role string `json:"role"`
Status string `json:"status"`
Groups *[]string `json:"groups"`
}
func (s *Server) requireSuperAdmin(c *gin.Context) {
s.configMu.RLock()
setupReady := s.config.SetupReady
s.configMu.RUnlock()
if !setupReady {
c.AbortWithStatusJSON(http.StatusConflict, gin.H{
"error": "请先完成初始化配置",
"request_id": requestIDFromContext(c),
})
return
}
session, ok := s.currentAuthSession(c)
if !ok {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "请先登录管理后台", "request_id": requestIDFromContext(c)})
return
}
if session.Role != repository.UserRoleSuperAdmin {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "只有超级管理员可以管理用户", "request_id": requestIDFromContext(c)})
return
}
if !s.sameOriginForAPI(c.Request) {
c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "请求来源校验失败", "request_id": requestIDFromContext(c)})
return
}
c.Next()
}
func (s *Server) listUsers(c *gin.Context) {
if s.users == nil {
s.writeError(c, http.StatusNotImplemented, "用户管理接口尚未配置", errors.New("用户仓储未配置"))
return
}
viewer, ok := s.userListViewer(c)
if !ok {
return
}
items, err := s.users.ListUsers(c.Request.Context())
if err != nil {
s.writeError(c, http.StatusInternalServerError, "查询用户失败", err)
return
}
visible := make([]repository.User, 0, len(items))
for _, item := range items {
if userVisibleTo(viewer, item) {
visible = append(visible, scopedUserGroups(viewer, item))
}
}
c.JSON(http.StatusOK, gin.H{"items": visible, "current_user_id": viewer.ID, "current_role": viewer.Role})
}
func (s *Server) getUser(c *gin.Context) {
if s.users == nil {
s.writeError(c, http.StatusNotImplemented, "用户管理接口尚未配置", errors.New("用户仓储未配置"))
return
}
viewer, ok := s.userListViewer(c)
if !ok {
return
}
item, err := s.users.GetUserByID(c.Request.Context(), c.Param("id"))
if err != nil {
if repository.IsNotFound(err) {
s.writeError(c, http.StatusNotFound, "用户不存在", err)
return
}
s.writeError(c, http.StatusInternalServerError, "读取用户失败", err)
return
}
if !userVisibleTo(viewer, item) {
s.writeError(c, http.StatusNotFound, "用户不存在", nil)
return
}
c.JSON(http.StatusOK, scopedUserGroups(viewer, item))
}
func (s *Server) createUser(c *gin.Context) {
if s.users == nil {
s.writeError(c, http.StatusNotImplemented, "用户管理接口尚未配置", errors.New("用户仓储未配置"))
return
}
var request userRequest
if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil {
s.writeError(c, http.StatusBadRequest, "用户参数无效", err)
return
}
item, err := userFromRequest(request, "")
if err != nil {
s.writeError(c, http.StatusBadRequest, "用户参数无效", err)
return
}
if item.Role == repository.UserRoleSuperAdmin {
s.writeError(c, http.StatusBadRequest, "系统只允许初始化时创建一个超级管理员", nil)
return
}
if !s.applyRequestedUserGroups(c, &item, request.Groups) {
return
}
if err := s.users.CreateUser(c.Request.Context(), item); err != nil {
s.writeError(c, userErrorStatus(err), "创建用户失败", err)
return
}
s.recordUserAudit(c, "创建用户", "success", item.ID)
c.JSON(http.StatusCreated, item)
}
func (s *Server) updateUser(c *gin.Context) {
if s.users == nil {
s.writeError(c, http.StatusNotImplemented, "用户管理接口尚未配置", errors.New("用户仓储未配置"))
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "primary-super-admin" {
s.writeError(c, http.StatusBadRequest, "主超级管理员不能通过用户管理修改", nil)
return
}
existing, err := s.users.GetUserByID(c.Request.Context(), id)
if err != nil {
if repository.IsNotFound(err) {
s.writeError(c, http.StatusNotFound, "用户不存在", err)
return
}
s.writeError(c, http.StatusInternalServerError, "读取用户失败", err)
return
}
var request userRequest
if err := bindJSON(c, &request, jsonRequestBodyLimit); err != nil {
s.writeError(c, http.StatusBadRequest, "用户参数无效", err)
return
}
item, err := userFromRequest(request, id)
if err != nil {
s.writeError(c, http.StatusBadRequest, "用户参数无效", err)
return
}
if request.Password == "" {
item.PasswordHash = existing.PasswordHash
}
if request.Role == "" {
item.Role = existing.Role
}
if request.Status == "" {
item.Status = existing.Status
}
item.Groups = existing.Groups
if !s.applyRequestedUserGroups(c, &item, request.Groups) {
return
}
if existing.Role != repository.UserRoleSuperAdmin && item.Role == repository.UserRoleSuperAdmin {
s.writeError(c, http.StatusBadRequest, "不能将用户提升为超级管理员", nil)
return
}
if existing.Role == repository.UserRoleSuperAdmin && item.Role != repository.UserRoleSuperAdmin {
s.writeError(c, http.StatusBadRequest, "不能降低超级管理员权限", nil)
return
}
if existing.Role == repository.UserRoleSuperAdmin && item.Status == repository.UserStatusDisabled {
if err := s.ensureAnotherSuperAdmin(c, id); err != nil {
s.writeError(c, http.StatusBadRequest, err.Error(), err)
return
}
}
if err := s.users.UpdateUser(c.Request.Context(), item); err != nil {
s.writeError(c, userErrorStatus(err), "更新用户失败", err)
return
}
s.invalidateUserSessions(id)
s.recordUserAudit(c, "更新用户", "success", id)
c.JSON(http.StatusOK, item)
}
func (s *Server) deleteUser(c *gin.Context) {
if s.users == nil {
s.writeError(c, http.StatusNotImplemented, "用户管理接口尚未配置", errors.New("用户仓储未配置"))
return
}
id := strings.TrimSpace(c.Param("id"))
session, _ := s.currentAuthSession(c)
if session.UserID != "" && session.UserID == id {
s.writeError(c, http.StatusBadRequest, "不能删除当前登录用户", nil)
return
}
existing, err := s.users.GetUserByID(c.Request.Context(), id)
if err != nil {
if repository.IsNotFound(err) {
s.writeError(c, http.StatusNotFound, "用户不存在", err)
return
}
s.writeError(c, http.StatusInternalServerError, "读取用户失败", err)
return
}
if existing.Role == repository.UserRoleSuperAdmin {
if existing.ID == "primary-super-admin" {
s.writeError(c, http.StatusBadRequest, "不能删除配置中的主超级管理员", repository.ErrSuperAdminProtected)
return
}
if err := s.ensureAnotherSuperAdmin(c, id); err != nil {
s.writeError(c, http.StatusBadRequest, err.Error(), err)
return
}
}
if err := s.users.DeleteUser(c.Request.Context(), id); err != nil {
if errors.Is(err, repository.ErrSuperAdminProtected) {
s.writeError(c, http.StatusBadRequest, err.Error(), err)
return
}
if repository.IsNotFound(err) {
s.writeError(c, http.StatusNotFound, "用户不存在", err)
return
}
s.writeError(c, userErrorStatus(err), "删除用户失败", err)
return
}
s.invalidateUserSessions(id)
s.recordUserAudit(c, "删除用户", "success", id)
c.Status(http.StatusNoContent)
}
func (s *Server) ensureAnotherSuperAdmin(c *gin.Context, currentID string) error {
items, err := s.users.ListUsers(c.Request.Context())
if err != nil {
return errors.New("无法确认超级管理员数量")
}
for _, item := range items {
if item.Role == repository.UserRoleSuperAdmin && item.Status == repository.UserStatusActive && item.ID != currentID {
return nil
}
}
return errors.New("不能禁用最后一个超级管理员")
}
func userFromRequest(request userRequest, id string) (repository.User, error) {
username := strings.TrimSpace(strings.ToLower(request.Username))
if username == "" {
return repository.User{}, errors.New("用户名不能为空")
}
if request.Password == "" && id == "" {
return repository.User{}, errors.New("密码不能为空")
}
item := repository.User{
ID: id, Username: username, Email: request.Email,
Role: strings.TrimSpace(strings.ToLower(request.Role)), Status: strings.TrimSpace(strings.ToLower(request.Status)),
}
if item.Role == "" {
item.Role = repository.UserRoleUser
}
if item.Status == "" {
item.Status = repository.UserStatusActive
}
if request.Password != "" {
if err := passwordpolicy.ValidateAccountPassword(request.Password, username); err != nil {
return repository.User{}, err
}
hash, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
if err != nil {
return repository.User{}, errors.New("生成密码摘要失败")
}
item.PasswordHash = string(hash)
}
if item.ID == "" {
item.ID = domain.NewID()
}
return item, nil
}
func (s *Server) recordUserAudit(c *gin.Context, action, status, id string) {
if s.audit == nil {
return
}
if err := s.audit.CreateAuditLog(c.Request.Context(), repository.AuditLog{
Action: action, ResourceType: "user", ResourceID: id, Status: status, Message: action,
}); err != nil && s.logger != nil {
s.logger.Warn("保存用户审计日志失败", "error", err)
}
}
func userErrorStatus(err error) int {
if repository.IsConflict(err) {
return http.StatusConflict
}
if repository.IsNotFound(err) {
return http.StatusNotFound
}
if strings.Contains(err.Error(), "不能为空") || strings.Contains(err.Error(), "无效") || strings.Contains(err.Error(), "长度") || strings.Contains(err.Error(), "不能") {
return http.StatusBadRequest
}
return http.StatusInternalServerError
}