mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-09-05 15:37:03 +08:00
功能: NodeHandler 新增批量创建/轮换 Token/install-token/预览端点
This commit is contained in:
@@ -5,18 +5,32 @@ import (
|
|||||||
stdhttp "net/http"
|
stdhttp "net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"backupx/server/internal/apperror"
|
||||||
|
"backupx/server/internal/installscript"
|
||||||
"backupx/server/internal/service"
|
"backupx/server/internal/service"
|
||||||
"backupx/server/pkg/response"
|
"backupx/server/pkg/response"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type NodeHandler struct {
|
type NodeHandler struct {
|
||||||
service *service.NodeService
|
service *service.NodeService
|
||||||
auditService *service.AuditService
|
auditService *service.AuditService
|
||||||
|
installTokenSvc *service.InstallTokenService
|
||||||
|
externalURL string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewNodeHandler(service *service.NodeService, auditService *service.AuditService) *NodeHandler {
|
func NewNodeHandler(
|
||||||
return &NodeHandler{service: service, auditService: auditService}
|
nodeService *service.NodeService,
|
||||||
|
auditService *service.AuditService,
|
||||||
|
installTokenSvc *service.InstallTokenService,
|
||||||
|
externalURL string,
|
||||||
|
) *NodeHandler {
|
||||||
|
return &NodeHandler{
|
||||||
|
service: nodeService,
|
||||||
|
auditService: auditService,
|
||||||
|
installTokenSvc: installTokenSvc,
|
||||||
|
externalURL: externalURL,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *NodeHandler) List(c *gin.Context) {
|
func (h *NodeHandler) List(c *gin.Context) {
|
||||||
@@ -128,3 +142,135 @@ func (h *NodeHandler) Heartbeat(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
response.Success(c, gin.H{"status": "ok"})
|
response.Success(c, gin.H{"status": "ok"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BatchCreate 批量创建远程节点。
|
||||||
|
func (h *NodeHandler) BatchCreate(c *gin.Context) {
|
||||||
|
var input struct {
|
||||||
|
Names []string `json:"names" binding:"required"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(stdhttp.StatusBadRequest, gin.H{"code": "INVALID_INPUT", "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results, err := h.service.BatchCreate(c.Request.Context(), input.Names)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recordAudit(c, h.auditService, "node", "batch_create", "node", "",
|
||||||
|
fmt.Sprintf("%d", len(results)), fmt.Sprintf("批量创建 %d 个节点", len(results)))
|
||||||
|
response.Success(c, results)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RotateToken 轮换节点的 agent token。
|
||||||
|
func (h *NodeHandler) RotateToken(c *gin.Context) {
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tok, err := h.service.RotateToken(c.Request.Context(), uint(id))
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recordAudit(c, h.auditService, "node", "rotate_token", "node",
|
||||||
|
fmt.Sprintf("%d", id), "",
|
||||||
|
fmt.Sprintf("轮换节点 Token (ID: %d)", id))
|
||||||
|
response.Success(c, gin.H{"newToken": tok})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInstallToken 生成一次性安装令牌。
|
||||||
|
func (h *NodeHandler) CreateInstallToken(c *gin.Context) {
|
||||||
|
if h.installTokenSvc == nil {
|
||||||
|
response.Error(c, apperror.New(stdhttp.StatusServiceUnavailable,
|
||||||
|
"INSTALL_TOKEN_DISABLED", "一键部署未启用", nil))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var input struct {
|
||||||
|
Mode string `json:"mode"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
AgentVersion string `json:"agentVersion"`
|
||||||
|
DownloadSrc string `json:"downloadSrc"`
|
||||||
|
TTLSeconds int `json:"ttlSeconds"`
|
||||||
|
}
|
||||||
|
if err := c.ShouldBindJSON(&input); err != nil {
|
||||||
|
c.JSON(stdhttp.StatusBadRequest, gin.H{"code": "INVALID_INPUT", "message": err.Error()})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 默认值
|
||||||
|
if input.Mode == "" {
|
||||||
|
input.Mode = "systemd"
|
||||||
|
}
|
||||||
|
if input.Arch == "" {
|
||||||
|
input.Arch = "auto"
|
||||||
|
}
|
||||||
|
if input.DownloadSrc == "" {
|
||||||
|
input.DownloadSrc = "github"
|
||||||
|
}
|
||||||
|
if input.TTLSeconds == 0 {
|
||||||
|
input.TTLSeconds = 900
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := h.installTokenSvc.Create(c.Request.Context(), service.InstallTokenInput{
|
||||||
|
NodeID: uint(id),
|
||||||
|
Mode: input.Mode,
|
||||||
|
Arch: input.Arch,
|
||||||
|
AgentVersion: input.AgentVersion,
|
||||||
|
DownloadSrc: input.DownloadSrc,
|
||||||
|
TTLSeconds: input.TTLSeconds,
|
||||||
|
CreatedByID: 0, // 如需关联 userID,后续可通过 auth 中间件注入
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recordAudit(c, h.auditService, "install_token", "create", "node",
|
||||||
|
fmt.Sprintf("%d", id), out.Node.Name,
|
||||||
|
fmt.Sprintf("生成 %s/%s install token TTL=%ds", input.Mode, input.Arch, input.TTLSeconds))
|
||||||
|
|
||||||
|
masterURL := resolveMasterURL(c, h.externalURL)
|
||||||
|
body := gin.H{
|
||||||
|
"installToken": out.Token,
|
||||||
|
"expiresAt": out.ExpiresAt,
|
||||||
|
"url": masterURL + "/install/" + out.Token,
|
||||||
|
"composeUrl": "",
|
||||||
|
}
|
||||||
|
if input.Mode == "docker" {
|
||||||
|
body["composeUrl"] = masterURL + "/install/" + out.Token + "/compose.yml"
|
||||||
|
}
|
||||||
|
response.Success(c, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreviewScript 预览安装脚本(token 字段用 <AGENT_TOKEN> 占位,不消费 install token)。
|
||||||
|
// 用于 UI Step 3 展开"脚本预览"。
|
||||||
|
func (h *NodeHandler) PreviewScript(c *gin.Context) {
|
||||||
|
mode := c.DefaultQuery("mode", "systemd")
|
||||||
|
arch := c.DefaultQuery("arch", "auto")
|
||||||
|
ver := c.Query("agentVersion")
|
||||||
|
if ver == "" {
|
||||||
|
c.JSON(stdhttp.StatusBadRequest, gin.H{"code": "INVALID_INPUT", "message": "agentVersion required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
src := c.DefaultQuery("downloadSrc", "github")
|
||||||
|
ctx := installscript.Context{
|
||||||
|
MasterURL: resolveMasterURL(c, h.externalURL),
|
||||||
|
AgentToken: "<AGENT_TOKEN>",
|
||||||
|
AgentVersion: ver,
|
||||||
|
Mode: mode,
|
||||||
|
Arch: arch,
|
||||||
|
DownloadBase: installscript.DownloadBaseFor(src),
|
||||||
|
InstallPrefix: "/opt/backupx-agent",
|
||||||
|
}
|
||||||
|
script, err := installscript.RenderScript(ctx)
|
||||||
|
if err != nil {
|
||||||
|
response.Error(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Data(stdhttp.StatusOK, "text/x-shellscript; charset=utf-8", []byte(script))
|
||||||
|
}
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ func NewRouter(deps RouterDependencies) *gin.Engine {
|
|||||||
database.POST("/discover", databaseHandler.Discover)
|
database.POST("/discover", databaseHandler.Discover)
|
||||||
}
|
}
|
||||||
|
|
||||||
nodeHandler := NewNodeHandler(deps.NodeService, deps.AuditService)
|
// 临时让 build 通过:InstallTokenService 与 externalURL 传 nil 和 "",Task 11 会替换成真实值
|
||||||
|
nodeHandler := NewNodeHandler(deps.NodeService, deps.AuditService, nil, "")
|
||||||
nodes := api.Group("/nodes")
|
nodes := api.Group("/nodes")
|
||||||
nodes.Use(AuthMiddleware(deps.JWTManager))
|
nodes.Use(AuthMiddleware(deps.JWTManager))
|
||||||
nodes.GET("", nodeHandler.List)
|
nodes.GET("", nodeHandler.List)
|
||||||
|
|||||||
Reference in New Issue
Block a user