Files
EveryPublish/server/internal/api/handlers/agent.go
T

148 lines
4.3 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 handlers
import (
"crypto/rand"
"errors"
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
"everypublish/server/internal/api/response"
"everypublish/server/internal/models"
"everypublish/server/internal/ws"
)
// AgentHandler 设备配对与列表
type AgentHandler struct {
DB *gorm.DB
Hub *ws.Hub
}
const pairAlphabet = "ABCDEFGHJKMNPQRSTUVWXYZ23456789"
var errPairCodeUsed = errors.New("pair code already used")
func randCode(n int) string {
b := make([]byte, n)
_, _ = rand.Read(b)
for i := range b {
b[i] = pairAlphabet[int(b[i])%len(pairAlphabet)]
}
return string(b)
}
// PairCode 生成配对码(5 分钟一次性)
func (h *AgentHandler) PairCode(c *gin.Context) {
pc := models.PairingCode{
WorkspaceID: c.GetUint64("wsid"),
Code: randCode(6),
ExpiresAt: time.Now().Add(5 * time.Minute),
}
if err := h.DB.Create(&pc).Error; err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "生成配对码失败")
return
}
response.Audit(c, "agent.pair_code", "pairing:"+itoa(pc.ID), gin.H{"code": pc.Code})
response.OK(c, gin.H{"code": pc.Code, "expiresAt": pc.ExpiresAt})
}
// Pair 设备配对(无鉴权,凭一次性码;注册 Ed25519 公钥)
func (h *AgentHandler) Pair(c *gin.Context) {
var req struct {
Code string `json:"code" binding:"required"`
DeviceName string `json:"deviceName" binding:"required"`
OS string `json:"os"`
Version string `json:"version"`
PublicKey string `json:"publicKey" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
return
}
var pc models.PairingCode
if err := h.DB.Where("code = ? and used = ?", req.Code, false).First(&pc).Error; err != nil {
response.Fail(c, http.StatusBadRequest, 2006, "配对码无效")
return
}
if time.Now().After(pc.ExpiresAt) {
response.Fail(c, http.StatusGone, 2006, "配对码已过期")
return
}
device := models.AgentDevice{
WorkspaceID: pc.WorkspaceID,
Name: req.DeviceName,
OS: req.OS,
Version: req.Version,
PublicKey: req.PublicKey,
Status: "offline",
PairedAt: time.Now(),
}
err := h.DB.Transaction(func(tx *gorm.DB) error {
// 原子占用:仅当仍 unused 且未过期时置 used=true;RowsAffected==1 才视为抢到,
// 防止并发用同一码配出多台设备。
res := tx.Model(&models.PairingCode{}).
Where("id = ? and used = ?", pc.ID, false).
Update("used", true)
if res.Error != nil {
return res.Error
}
if res.RowsAffected != 1 {
return errPairCodeUsed
}
if err := tx.Create(&device).Error; err != nil {
return err
}
if err := tx.Model(&models.PairingCode{}).Where("id = ?", pc.ID).
Update("device_id", device.ID).Error; err != nil {
return err
}
return nil
})
if err != nil {
if err == errPairCodeUsed {
response.Fail(c, http.StatusBadRequest, 2006, "配对码已被使用")
return
}
response.Fail(c, http.StatusInternalServerError, 5000, "配对失败")
return
}
response.OK(c, gin.H{"deviceId": device.ID, "workspaceId": device.WorkspaceID})
}
// Devices 设备列表(在线状态来自 Hub,此处以 DB 状态兜底)
func (h *AgentHandler) Devices(c *gin.Context) {
var list []models.AgentDevice
h.DB.Where("workspace_id = ?", c.GetUint64("wsid")).Order("id desc").Find(&list)
response.OK(c, gin.H{"list": list})
}
// Revoke 吊销设备并强制断开(owner/admin)
func (h *AgentHandler) Revoke(c *gin.Context) {
if !isAdminRole(c.GetString("mrole")) {
response.Fail(c, http.StatusForbidden, 1003, "仅管理员可吊销设备")
return
}
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
response.Fail(c, http.StatusBadRequest, 1001, "参数不合法")
return
}
var device models.AgentDevice
if err = h.DB.Where("id = ? and workspace_id = ?", id, c.GetUint64("wsid")).First(&device).Error; err != nil {
response.Fail(c, http.StatusNotFound, 1004, "设备不存在")
return
}
if err = h.DB.Model(&device).Updates(map[string]interface{}{"revoked": true, "status": "offline"}).Error; err != nil {
response.Fail(c, http.StatusInternalServerError, 5000, "吊销失败")
return
}
if h.Hub != nil {
h.Hub.KickAgent(device.ID)
}
response.Audit(c, "agent.revoke", "device:"+itoa(id), nil)
response.OK(c, nil)
}