Files
user-system/internal/api/handler/device_handler.go
long-agent 8095307d82 fix: P0/P1 security and quality fixes
P0-01: Add ESCAPE clause to LIKE queries in operation_log.go and device.go
P0-02: Add atomic Increment to L1Cache and L2Cache interfaces
P0-07: Add TOTP verification step after password login
P1-01: Sanitize error messages in error.go middleware
P1-03: Remove err.Error() from export error messages
P1-04: Add error return to CountByResultSince in login_log.go
P1-05: Add transactional DeleteCascade to RoleRepository
P1-06: Add PasswordChangedAt tracking for JWT token invalidation
P1-07: Wrap theme SetDefault in database transaction
P1-08: Use config values for database pool parameters
P1-09: Add rows.Err() checks in social_account_repo.go
P1-10: Validate sortOrder with map in user.go ORDER BY
P1-11: Add GORM tags to Announcement struct
P1-15: Add pageSize upper limit (100) to device and log handlers
2026-04-18 15:33:12 +08:00

578 lines
16 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 (
"fmt"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"github.com/user-management-system/internal/domain"
"github.com/user-management-system/internal/service"
)
// DeviceHandler handles device management requests
type DeviceHandler struct {
deviceService *service.DeviceService
}
// NewDeviceHandler creates a new DeviceHandler
func NewDeviceHandler(deviceService *service.DeviceService) *DeviceHandler {
return &DeviceHandler{deviceService: deviceService}
}
// CreateDevice 创建设备
// @Summary 创建设备记录
// @Description 当前用户创建设备记录
// @Tags 设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body service.CreateDeviceRequest true "设备信息"
// @Success 201 {object} Response{data=domain.Device} "设备创建成功"
// @Failure 401 {object} Response "未认证"
// @Router /api/v1/devices [post]
func (h *DeviceHandler) CreateDevice(c *gin.Context) {
userID, ok := getUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
var req service.CreateDeviceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
device, err := h.deviceService.CreateDevice(c.Request.Context(), userID, &req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusCreated, gin.H{
"code": 0,
"message": "success",
"data": device,
})
}
// GetMyDevices 获取我的设备列表
// @Summary 获取当前用户的设备列表
// @Description 获取当前用户的所有设备记录
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} Response{data=DeviceListResponse} "设备列表"
// @Failure 401 {object} Response "未认证"
// @Router /api/v1/devices [get]
func (h *DeviceHandler) GetMyDevices(c *gin.Context) {
userID, ok := getUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
devices, total, err := h.deviceService.GetUserDevices(c.Request.Context(), userID, page, pageSize)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": gin.H{
"items": devices,
"total": total,
"page": page,
"page_size": pageSize,
},
})
}
// GetDevice 获取设备详情
// @Summary 获取设备详情
// @Description 根据ID获取设备详细信息
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "设备ID"
// @Success 200 {object} Response{data=domain.Device} "设备信息"
// @Failure 404 {object} Response "设备不存在"
// @Router /api/v1/devices/{id} [get]
func (h *DeviceHandler) GetDevice(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 device id"})
return
}
device, err := h.deviceService.GetDevice(c.Request.Context(), id)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": device,
})
}
// UpdateDevice 更新设备
// @Summary 更新设备信息
// @Description 更新设备的基本信息
// @Tags 设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "设备ID"
// @Param request body service.UpdateDeviceRequest true "更新信息"
// @Success 200 {object} Response{data=domain.Device} "更新成功"
// @Failure 400 {object} Response "请求参数错误"
// @Failure 404 {object} Response "设备不存在"
// @Router /api/v1/devices/{id} [put]
func (h *DeviceHandler) UpdateDevice(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 device id"})
return
}
var req service.UpdateDeviceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
device, err := h.deviceService.UpdateDevice(c.Request.Context(), id, &req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": device,
})
}
// DeleteDevice 删除设备
// @Summary 删除设备
// @Description 删除设备记录
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "设备ID"
// @Success 200 {object} Response "删除成功"
// @Failure 404 {object} Response "设备不存在"
// @Router /api/v1/devices/{id} [delete]
func (h *DeviceHandler) DeleteDevice(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 device id"})
return
}
if err := h.deviceService.DeleteDevice(c.Request.Context(), id); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "device deleted",
})
}
// UpdateDeviceStatus 更新设备状态
// @Summary 更新设备状态
// @Description 更新设备状态active/inactive
// @Tags 设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "设备ID"
// @Param request body UpdateDeviceStatusRequest true "状态信息"
// @Success 200 {object} Response "状态更新成功"
// @Failure 400 {object} Response "无效的状态值"
// @Failure 404 {object} Response "设备不存在"
// @Router /api/v1/devices/{id}/status [put]
func (h *DeviceHandler) UpdateDeviceStatus(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 device 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.DeviceStatus
switch req.Status {
case "active", "1":
status = domain.DeviceStatusActive
case "inactive", "0":
status = domain.DeviceStatusInactive
default:
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid status"})
return
}
if err := h.deviceService.UpdateDeviceStatus(c.Request.Context(), id, status); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "status updated",
})
}
// GetUserDevices 获取指定用户的设备列表
// @Summary 获取用户设备列表
// @Description 获取指定用户的设备列表(仅本人或管理员)
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "用户ID"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} Response{data=DeviceListResponse} "设备列表"
// @Failure 403 {object} Response "无权限"
// @Router /api/v1/users/{id}/devices [get]
func (h *DeviceHandler) GetUserDevices(c *gin.Context) {
// IDOR 修复:检查当前用户是否有权限查看指定用户的设备
currentUserID, ok := getUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
// 检查是否为管理员
roleCodes, _ := c.Get("role_codes")
isAdmin := false
if roles, ok := roleCodes.([]string); ok {
for _, role := range roles {
if role == "admin" {
isAdmin = true
break
}
}
}
userIDParam := c.Param("id")
userID, err := strconv.ParseInt(userIDParam, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid user id"})
return
}
// 非管理员只能查看自己的设备
if !isAdmin && userID != currentUserID {
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权访问该用户的设备列表"})
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
devices, total, err := h.deviceService.GetUserDevices(c.Request.Context(), userID, page, pageSize)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": gin.H{
"items": devices,
"total": total,
"page": page,
"page_size": pageSize,
},
})
}
// GetAllDevices 获取所有设备列表
// @Summary 获取所有设备列表
// @Description 获取所有设备列表(仅管理员),支持游标分页和偏移分页
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Param cursor query string false "游标分页游标"
// @Param size query int false "每页数量(游标模式)"
// @Param page query int false "页码"
// @Param page_size query int false "每页数量"
// @Success 200 {object} Response{data=DeviceListResponse} "设备列表"
// @Failure 403 {object} Response "无权限"
// @Router /api/v1/admin/devices [get]
func (h *DeviceHandler) GetAllDevices(c *gin.Context) {
var req service.GetAllDevicesRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
// Use cursor-based pagination when cursor is provided
if req.Cursor != "" || req.Size > 0 {
result, err := h.deviceService.GetAllDevicesCursor(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
devices, total, err := h.deviceService.GetAllDevices(c.Request.Context(), &req)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": gin.H{
"items": devices,
"total": total,
"page": req.Page,
"page_size": req.PageSize,
},
})
}
// TrustDeviceRequest 信任设备请求
type TrustDeviceRequest struct {
TrustDuration string `json:"trust_duration"` // 信任持续时间,如 "30d" 表示30天
}
// TrustDevice 设置设备为信任设备
// @Summary 设置设备为信任设备
// @Description 将指定设备设置为信任设备,在信任期内免二次验证
// @Tags 设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "设备ID"
// @Param request body TrustDeviceRequest true "信任配置"
// @Success 200 {object} Response "设置成功"
// @Failure 404 {object} Response "设备不存在"
// @Router /api/v1/devices/{id}/trust [post]
func (h *DeviceHandler) TrustDevice(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 device id"})
return
}
var req TrustDeviceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
// 解析信任持续时间
trustDuration := parseDuration(req.TrustDuration)
if err := h.deviceService.TrustDevice(c.Request.Context(), id, trustDuration); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "device trusted",
})
}
// TrustDeviceByDeviceID 根据设备标识设置设备为信任状态
// @Summary 根据设备标识设置信任
// @Description 根据设备唯一标识字符串设置设备为信任状态
// @Tags 设备管理
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param deviceId path string true "设备唯一标识"
// @Param request body TrustDeviceRequest true "信任配置"
// @Success 200 {object} Response "设置成功"
// @Failure 401 {object} Response "未认证"
// @Router /api/v1/devices/trust/{deviceId} [post]
func (h *DeviceHandler) TrustDeviceByDeviceID(c *gin.Context) {
userID, ok := getUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
deviceID := c.Param("deviceId")
if deviceID == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid device id"})
return
}
var req TrustDeviceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": err.Error()})
return
}
// 解析信任持续时间
trustDuration := parseDuration(req.TrustDuration)
if err := h.deviceService.TrustDeviceByDeviceID(c.Request.Context(), userID, deviceID, trustDuration); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "device trusted",
})
}
// UntrustDevice 取消设备信任状态
// @Summary 取消设备信任
// @Description 取消设备的信任状态
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Param id path int true "设备ID"
// @Success 200 {object} Response "取消成功"
// @Failure 404 {object} Response "设备不存在"
// @Router /api/v1/devices/{id}/trust [delete]
func (h *DeviceHandler) UntrustDevice(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 device id"})
return
}
if err := h.deviceService.UntrustDevice(c.Request.Context(), id); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "device untrusted",
})
}
// GetMyTrustedDevices 获取我的信任设备列表
// @Summary 获取信任设备列表
// @Description 获取当前用户的信任设备列表
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Success 200 {object} Response{data=[]domain.Device} "信任设备列表"
// @Failure 401 {object} Response "未认证"
// @Router /api/v1/devices/trusted [get]
func (h *DeviceHandler) GetMyTrustedDevices(c *gin.Context) {
userID, ok := getUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
devices, err := h.deviceService.GetTrustedDevices(c.Request.Context(), userID)
if err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "success",
"data": devices,
})
}
// LogoutAllOtherDevices 登出所有其他设备
// @Summary 登出其他设备
// @Description 登出当前用户除指定设备外的所有其他设备
// @Tags 设备管理
// @Produce json
// @Security BearerAuth
// @Param X-Device-ID header string true "当前设备ID"
// @Success 200 {object} Response "登出成功"
// @Failure 400 {object} Response "无效的设备ID"
// @Failure 401 {object} Response "未认证"
// @Router /api/v1/devices/logout-others [post]
func (h *DeviceHandler) LogoutAllOtherDevices(c *gin.Context) {
userID, ok := getUserIDFromContext(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "unauthorized"})
return
}
// 从请求中获取当前设备ID
currentDeviceIDStr := c.GetHeader("X-Device-ID")
currentDeviceID, err := strconv.ParseInt(currentDeviceIDStr, 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "invalid current device id"})
return
}
if err := h.deviceService.LogoutAllOtherDevices(c.Request.Context(), userID, currentDeviceID); err != nil {
handleError(c, err)
return
}
c.JSON(http.StatusOK, gin.H{
"code": 0,
"message": "all other devices logged out",
})
}
// parseDuration 解析duration字符串如 "30d" -> 30天的time.Duration
func parseDuration(s string) time.Duration {
if s == "" {
return 0
}
// 简单实现,支持 d(天)和h(小时)
var d int
var h int
_, _ = d, h
switch s[len(s)-1] {
case 'd':
d = 1
_, _ = fmt.Sscanf(s[:len(s)-1], "%d", &d)
return time.Duration(d) * 24 * time.Hour
case 'h':
_, _ = fmt.Sscanf(s[:len(s)-1], "%d", &h)
return time.Duration(h) * time.Hour
}
return 0
}