Files
user-system/internal/api/handler/user_handler.go
long-agent 0795e126cc fix: resolve P0 security issues per governance baseline
P0-01: LIKE injection fix in device.go (2 locations)
- Added escapeLikePattern() to prevent LIKE pattern manipulation

P0-03: Token refresh blacklist fail-closed
- RefreshToken() now returns error if cache.Set fails
- Prevents token double-spend on cache failures

P0-05: CORS dangerous default configuration
- Default changed to empty origins, credentials off
- init() panics if default config is dangerous

P0-06: UpdateUser IDOR vulnerability fix
- Added authorization check (self-or-admin)
- Prevents unauthorized user profile modification

Also: Fixed frontend lint errors in device-fingerprint.test.ts and http/index.test.ts

All 518 frontend tests pass, all backend tests pass.
2026-04-18 09:32:54 +08:00

608 lines
18 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 handler
import (
"net/http"
"strconv"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/auth"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/service"
)
// UserHandler handles user management requests
type UserHandler struct {
userService *service.UserService
}
// NewUserHandler creates a new UserHandler
func NewUserHandler(userService *service.UserService) *UserHandler {
return &UserHandler{userService: userService}
}
// CreateUser 创建用户
// @Summary 创建用户
// @Description 创建新用户账号(仅管理员)
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CreateUserRequest true "用户信息"
// @Success 201 {object} Response{data=UserResponse} "用户创建成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 401 {object} Response "未认证"
// @Failure 403 {object} Response "无权限"
// @Router /api/v1/users [post]
func (h *UserHandler) CreateUser(c *gin.Context) {
var req struct {
Username string `json:"username" binding:"required"`
Email string `json:"email"`
Password string `json:"password"`
Nickname string `json:"nickname"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
user := &domain.User{
Username: req.Username,
Email: domain.StrPtr(req.Email),
Nickname: req.Nickname,
Status: domain.UserStatusActive,
}
if req.Password != "" {
hashed, err := auth.HashPassword(req.Password)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to hash password"})
return
}
user.Password = hashed
}
if err := h.userService.Create(c.Request.Context(), user); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusCreated, gin.H{
"code": 0,
"message": "success",
"data": toUserResponse(user),
})
}
// ListUsers 获取用户列表
// @Summary 获取用户列表
// @Description 获取用户列表,支持游标分页和偏移分页
// @Tags 用户管理
// @Produce json
// @Security BearerAuth
// @Param cursor query string false "游标分页游标"
// @Param size query int false "每页大小"
// @Param offset query int false "偏移分页偏移量"
// @Param limit query int false "每页大小"
// @Success 200 {object} Response{data=UserListResponse} "用户列表"
// @Router /api/v1/users [get]
func (h *UserHandler) ListUsers(c *gin.Context) {
cursor := c.Query("cursor")
sizeStr := c.DefaultQuery("size", "")
// Use cursor-based pagination when cursor is provided
if cursor != "" || sizeStr != "" {
var req service.ListCursorRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
result, err := h.userService.ListCursor(c.Request.Context(), &req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": result,
})
return
}
// Fallback to legacy offset-based pagination
offset, _ := strconv.ParseInt(c.DefaultQuery("offset", "0"), 10, 64)
limit, _ := strconv.ParseInt(c.DefaultQuery("limit", "20"), 10, 64)
users, total, err := h.userService.List(c.Request.Context(), int(offset), int(limit))
if err != nil {
handleError(c, err)
return
}
userResponses := make([]*UserResponse, len(users))
for i, u := range users {
userResponses[i] = toUserResponse(u)
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": gin.H{
"users": userResponses,
"total": total,
"offset": offset,
"limit": limit,
},
})
}
// GetUser 获取用户详情
// @Summary 获取用户详情
// @Description 根据ID获取用户详细信息
// @Tags 用户管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Success 200 {object} Response{data=UserResponse} "用户信息"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id} [get]
func (h *UserHandler) GetUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": toUserResponse(user)})
}
// UpdateUser 更新用户
// @Summary 更新用户信息
// @Description 更新用户的基本信息(仅管理员或本人)
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Param request body UpdateUserRequest true "更新信息"
// @Success 200 {object} Response{data=UserResponse} "更新成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 403 {object} Response "无权限"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id} [put]
func (h *UserHandler) UpdateUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
// Authorization: only self or admin can update user profile
currentUserID := c.GetInt64("user_id")
isAdmin := false
if roles, ok := c.Get("user_roles"); ok {
for _, role := range roles.([]*domain.Role) {
if role.Code == "admin" {
isAdmin = true
break
}
}
}
if currentUserID != id && !isAdmin {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "permission denied"})
return
}
var req struct {
Email *string `json:"email"`
Nickname *string `json:"nickname"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
user, err := h.userService.GetByID(c.Request.Context(), id)
if err != nil {
handleError(c, err)
return
}
if req.Email != nil {
user.Email = req.Email
}
if req.Nickname != nil {
user.Nickname = *req.Nickname
}
if err := h.userService.Update(c.Request.Context(), user); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": toUserResponse(user)})
}
// DeleteUser 删除用户
// @Summary 删除用户
// @Description 删除用户账号(仅管理员)
// @Tags 用户管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Success 200 {object} Response "删除成功"
// @Failure 403 {object} Response "无权限"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id} [delete]
func (h *UserHandler) DeleteUser(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
if err := h.userService.Delete(c.Request.Context(), id); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"})
}
// UpdatePassword 修改密码
// @Summary 修改用户密码
// @Description 修改用户密码(仅管理员或本人)
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Param request body UpdatePasswordRequest true "密码信息"
// @Success 200 {object} Response "密码修改成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 403 {object} Response "无权限"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id}/password [put]
func (h *UserHandler) UpdatePassword(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
var req struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
if err := h.userService.ChangePassword(c.Request.Context(), id, req.OldPassword, req.NewPassword); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "密码修改成功"})
}
// UpdateUserStatus 更新用户状态
// @Summary 更新用户状态
// @Description 更新用户账号状态active/inactive/locked/disabled仅管理员
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Param request body UpdateStatusRequest true "状态信息"
// @Success 200 {object} Response "状态更新成功"
// @Failure 400 {object} Response "无效的状态值"
// @Failure 403 {object} Response "无权限"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id}/status [put]
func (h *UserHandler) UpdateUserStatus(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
var req struct {
Status string `json:"status" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
var status domain.UserStatus
switch req.Status {
case "active", "1":
status = domain.UserStatusActive
case "inactive", "0":
status = domain.UserStatusInactive
case "locked", "2":
status = domain.UserStatusLocked
case "disabled", "3":
status = domain.UserStatusDisabled
default:
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid status"})
return
}
if err := h.userService.UpdateStatus(c.Request.Context(), id, status); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success"})
}
// GetUserRoles 获取用户角色
// @Summary 获取用户角色列表
// @Description 获取指定用户的角色列表(仅本人或管理员)
// @Tags 用户管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Success 200 {object} Response{data=[]domain.Role} "角色列表"
// @Failure 403 {object} Response "无权限"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id}/roles [get]
func (h *UserHandler) GetUserRoles(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
// Authorization: only self or admin can view user roles
currentUserID := c.GetInt64("user_id")
isAdmin := false
if roles, ok := c.Get("user_roles"); ok {
for _, role := range roles.([]*domain.Role) {
if role.Code == "admin" {
isAdmin = true
break
}
}
}
if currentUserID != id && !isAdmin {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "permission denied"})
return
}
roles, err := h.userService.GetUserRoles(c.Request.Context(), id)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": roles,
})
}
// AssignRoles 分配用户角色
// @Summary 分配用户角色
// @Description 为用户分配角色(替换现有角色)(仅管理员)
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Param request body AssignRolesRequest true "角色ID列表"
// @Success 200 {object} Response "角色分配成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 403 {object} Response "无权限"
// @Failure 404 {object} Response "用户不存在"
// @Router /api/v1/users/{id}/roles [post]
func (h *UserHandler) AssignRoles(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
var req struct {
RoleIDs []int64 `json:"role_ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
if err := h.userService.AssignRoles(c.Request.Context(), id, req.RoleIDs); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "角色分配成功"})
}
// BatchUpdateStatus 批量更新用户状态
// @Summary 批量更新用户状态
// @Description 批量更新多个用户的状态(仅管理员)
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body service.BatchUpdateStatusRequest true "批量更新请求"
// @Success 200 {object} Response "批量更新成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 403 {object} Response "无权限"
// @Router /api/v1/users/batch/status [put]
func (h *UserHandler) BatchUpdateStatus(c *gin.Context) {
var req service.BatchUpdateStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
count, err := h.userService.BatchUpdateStatus(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "批量更新失败"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "更新成功", "data": gin.H{"count": count}})
}
// BatchDelete 批量删除用户
// @Summary 批量删除用户
// @Description 批量删除多个用户(仅管理员)
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body service.BatchDeleteRequest true "批量删除请求"
// @Success 200 {object} Response "批量删除成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 403 {object} Response "无权限"
// @Router /api/v1/users/batch [delete]
func (h *UserHandler) BatchDelete(c *gin.Context) {
var req service.BatchDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
count, err := h.userService.BatchDelete(c.Request.Context(), &req)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "批量删除失败"})
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "删除成功", "data": gin.H{"count": count}})
}
// ListAdmins 获取管理员列表
// @Summary 获取管理员列表
// @Description 获取所有管理员用户列表(仅管理员)
// @Tags 用户管理
// @Produce json
// @Security BearerAuth
// @Success 200 {object} Response{data=[]UserResponse} "管理员列表"
// @Failure 403 {object} Response "无权限"
// @Router /api/v1/users/admins [get]
func (h *UserHandler) ListAdmins(c *gin.Context) {
admins, err := h.userService.ListAdmins(c.Request.Context())
if err != nil {
handleError(c, err)
return
}
adminResponses := make([]*UserResponse, len(admins))
for i, u := range admins {
adminResponses[i] = toUserResponse(u)
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "success", "data": adminResponses})
}
// CreateAdmin 创建管理员
// @Summary 创建管理员
// @Description 创建新管理员账号(仅管理员)
// @Tags 用户管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body CreateAdminRequest true "管理员信息"
// @Success 201 {object} Response{data=UserResponse} "管理员创建成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 403 {object} Response "无权限"
// @Router /api/v1/users/admins [post]
func (h *UserHandler) CreateAdmin(c *gin.Context) {
var req struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
Email string `json:"email"`
Nickname string `json:"nickname"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
adminReq := &service.CreateAdminRequest{
Username: req.Username,
Password: req.Password,
Email: req.Email,
Nickname: req.Nickname,
}
admin, err := h.userService.CreateAdmin(c.Request.Context(), adminReq)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusCreated, gin.H{"code": 0, "message": "管理员创建成功", "data": toUserResponse(admin)})
}
// DeleteAdmin 删除管理员
// @Summary 删除管理员
// @Description 删除管理员角色(最后管理员保护、自删保护)(仅管理员)
// @Tags 用户管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Success 200 {object} Response "管理员已移除"
// @Failure 400 {object} Response "无效的用户ID"
// @Failure 403 {object} Response "无权限"
// @Failure 409 {object} Response "无法删除(最后管理员或自删)"
// @Router /api/v1/users/admins/{id} [delete]
func (h *UserHandler) DeleteAdmin(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
currentUserID := c.GetInt64("user_id")
if err := h.userService.DeleteAdmin(c.Request.Context(), id, currentUserID); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{"code": 0, "message": "管理员已移除"})
}
type UserResponse struct {
ID int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email,omitempty"`
Nickname string `json:"nickname,omitempty"`
Status string `json:"status"`
}
func toUserResponse(u *domain.User) *UserResponse {
email := ""
if u.Email != nil {
email = *u.Email
}
return &UserResponse{
ID: u.ID,
Username: u.Username,
Email: email,
Nickname: u.Nickname,
Status: strconv.FormatInt(int64(u.Status), 10),
}
}