mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
174 lines
5.5 KiB
Go
174 lines
5.5 KiB
Go
package handler
|
|
|
|
import (
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/truewhile/MeBox/internal/middleware"
|
|
"github.com/truewhile/MeBox/internal/model"
|
|
"github.com/truewhile/MeBox/internal/service"
|
|
)
|
|
|
|
// 设备管理接口。
|
|
//
|
|
// 普通用户只能操作自己的设备(路由挂在 /me 下,用户 ID 始终取自会话);
|
|
// 管理员通过 /admin/users/:id/devices 代管任意用户。两组接口共用同一份
|
|
// DeviceService,因此「谁上线过、谁被踢掉」只有一处事实来源。
|
|
|
|
// deviceListPayload 是设备列表的下发形状。Fingerprint 不外发:它是防共享
|
|
// 判定用的内部标识,暴露出去只会方便伪造。
|
|
type deviceListPayload struct {
|
|
Devices []devicePayload `json:"devices"`
|
|
}
|
|
|
|
type devicePayload struct {
|
|
ID string `json:"id"`
|
|
DeviceID string `json:"device_id"`
|
|
DeviceName string `json:"device_name,omitempty"`
|
|
Client string `json:"client,omitempty"`
|
|
LastIP string `json:"last_ip,omitempty"`
|
|
LastSeenAt string `json:"last_seen_at,omitempty"`
|
|
LastPlayAt string `json:"last_play_at,omitempty"`
|
|
Kicked bool `json:"kicked"`
|
|
Online bool `json:"online"`
|
|
Playing bool `json:"playing"`
|
|
Warnings int `json:"warnings"`
|
|
}
|
|
|
|
func toDevicePayload(d model.UserDevice) devicePayload {
|
|
out := devicePayload{
|
|
ID: d.ID,
|
|
DeviceID: d.DeviceID,
|
|
DeviceName: d.DeviceName,
|
|
Client: d.Client,
|
|
LastIP: d.LastIP,
|
|
Kicked: d.Kicked,
|
|
Online: d.Online,
|
|
Playing: d.Playing,
|
|
Warnings: d.Warnings,
|
|
}
|
|
if !d.LastSeenAt.IsZero() {
|
|
out.LastSeenAt = d.LastSeenAt.Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
if d.LastPlayAt != nil && !d.LastPlayAt.IsZero() {
|
|
out.LastPlayAt = d.LastPlayAt.Format("2006-01-02T15:04:05Z07:00")
|
|
}
|
|
return out
|
|
}
|
|
|
|
func deviceListResponse(devices []model.UserDevice) deviceListPayload {
|
|
items := make([]devicePayload, 0, len(devices))
|
|
for _, d := range devices {
|
|
items = append(items, toDevicePayload(d))
|
|
}
|
|
return deviceListPayload{Devices: items}
|
|
}
|
|
|
|
// myDevicesHandler 返回当前会话用户的设备列表。
|
|
func myDevicesHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID := sessionUserID(c)
|
|
devices, err := svc.Device.ListDevices(c.Request.Context(), userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, deviceListResponse(devices))
|
|
}
|
|
}
|
|
|
|
// myKickDeviceHandler 踢掉当前用户的一台设备。
|
|
func myKickDeviceHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID := sessionUserID(c)
|
|
deviceID := strings.TrimSpace(c.Param("deviceID"))
|
|
if deviceID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "device id required"})
|
|
return
|
|
}
|
|
if err := svc.Device.KickDevice(c.Request.Context(), userID, deviceID); err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// myKickAllDevicesHandler 踢掉当前用户的全部设备。
|
|
func myKickAllDevicesHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID := sessionUserID(c)
|
|
if err := svc.Device.KickAllDevices(c.Request.Context(), userID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// adminUserDevicesHandler 返回指定用户的设备列表。
|
|
func adminUserDevicesHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID := strings.TrimSpace(c.Param("id"))
|
|
// FindByID 对「不存在」返回 (nil, nil),必须判空而不是判 error,
|
|
// 否则「用户不存在」会伪装成「该用户没有设备」的空列表。
|
|
user, err := svc.Repo.User.FindByID(c.Request.Context(), userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
if user == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "user not found"})
|
|
return
|
|
}
|
|
devices, err := svc.Device.ListDevices(c.Request.Context(), userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.JSON(http.StatusOK, deviceListResponse(devices))
|
|
}
|
|
}
|
|
|
|
// adminKickUserDeviceHandler 由管理员踢掉指定用户的一台设备。
|
|
func adminKickUserDeviceHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID := strings.TrimSpace(c.Param("id"))
|
|
deviceID := strings.TrimSpace(c.Param("deviceID"))
|
|
if deviceID == "" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "device id required"})
|
|
return
|
|
}
|
|
if err := svc.Device.KickDevice(c.Request.Context(), userID, deviceID); err != nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// adminKickAllUserDevicesHandler 由管理员踢掉指定用户的全部设备。
|
|
func adminKickAllUserDevicesHandler(svc *service.Container) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
userID := strings.TrimSpace(c.Param("id"))
|
|
if err := svc.Device.KickAllDevices(c.Request.Context(), userID); err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
c.Status(http.StatusNoContent)
|
|
}
|
|
}
|
|
|
|
// sessionUserID 读取会话用户 ID。调用方路由都挂在鉴权中间件之后,因此这里
|
|
// 只做类型断言兜底,不做权限判断。
|
|
func sessionUserID(c *gin.Context) string {
|
|
if v, ok := c.Get(middleware.CtxUserID); ok {
|
|
if s, ok := v.(string); ok {
|
|
return s
|
|
}
|
|
}
|
|
return ""
|
|
}
|