mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-05-07 08:12:56 +08:00
304 lines
8.6 KiB
Go
304 lines
8.6 KiB
Go
package http
|
||
|
||
import (
|
||
"fmt"
|
||
stdhttp "net/http"
|
||
"strconv"
|
||
|
||
"backupx/server/internal/apperror"
|
||
"backupx/server/internal/installscript"
|
||
"backupx/server/internal/repository"
|
||
"backupx/server/internal/service"
|
||
"backupx/server/pkg/response"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type NodeHandler struct {
|
||
service *service.NodeService
|
||
auditService *service.AuditService
|
||
installTokenSvc *service.InstallTokenService
|
||
userRepo repository.UserRepository
|
||
externalURL string
|
||
}
|
||
|
||
// NewNodeHandler 构造 handler。
|
||
// userRepo 用于把 JWT subject(用户名)解析为 user.ID,填入 install_token.created_by_id 做审计追溯;
|
||
// 传 nil 时 created_by_id 记为 0(仍可用,不阻断)。
|
||
func NewNodeHandler(
|
||
nodeService *service.NodeService,
|
||
auditService *service.AuditService,
|
||
installTokenSvc *service.InstallTokenService,
|
||
userRepo repository.UserRepository,
|
||
externalURL string,
|
||
) *NodeHandler {
|
||
return &NodeHandler{
|
||
service: nodeService,
|
||
auditService: auditService,
|
||
installTokenSvc: installTokenSvc,
|
||
userRepo: userRepo,
|
||
externalURL: externalURL,
|
||
}
|
||
}
|
||
|
||
// resolveCurrentUserID 从 JWT subject 解析出 user.ID,失败返回 0。
|
||
func (h *NodeHandler) resolveCurrentUserID(c *gin.Context) uint {
|
||
if h.userRepo == nil {
|
||
return 0
|
||
}
|
||
subjectValue, ok := c.Get(contextUserSubjectKey)
|
||
if !ok {
|
||
return 0
|
||
}
|
||
subject, err := service.SubjectFromContextValue(subjectValue)
|
||
if err != nil || subject == "" {
|
||
return 0
|
||
}
|
||
user, err := h.userRepo.FindByUsername(c.Request.Context(), subject)
|
||
if err != nil || user == nil {
|
||
return 0
|
||
}
|
||
return user.ID
|
||
}
|
||
|
||
func (h *NodeHandler) List(c *gin.Context) {
|
||
items, err := h.service.List(c.Request.Context())
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
response.Success(c, items)
|
||
}
|
||
|
||
func (h *NodeHandler) Get(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
item, err := h.service.Get(c.Request.Context(), uint(id))
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
response.Success(c, item)
|
||
}
|
||
|
||
func (h *NodeHandler) Create(c *gin.Context) {
|
||
var input service.NodeCreateInput
|
||
if err := c.ShouldBindJSON(&input); err != nil {
|
||
c.JSON(stdhttp.StatusBadRequest, gin.H{"code": "INVALID_INPUT", "message": err.Error()})
|
||
return
|
||
}
|
||
token, err := h.service.Create(c.Request.Context(), input)
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
recordAudit(c, h.auditService, "node", "create", "node", "", input.Name,
|
||
fmt.Sprintf("创建远程节点「%s」", input.Name))
|
||
response.Success(c, gin.H{"token": token})
|
||
}
|
||
|
||
func (h *NodeHandler) Delete(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
if err := h.service.Delete(c.Request.Context(), uint(id)); err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
recordAudit(c, h.auditService, "node", "delete", "node", fmt.Sprintf("%d", id), "",
|
||
fmt.Sprintf("删除节点 (ID: %d)", id))
|
||
response.Success(c, nil)
|
||
}
|
||
|
||
func (h *NodeHandler) ListDirectory(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
path := c.DefaultQuery("path", "/")
|
||
entries, err := h.service.ListDirectory(c.Request.Context(), uint(id), path)
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
response.Success(c, entries)
|
||
}
|
||
|
||
func (h *NodeHandler) Update(c *gin.Context) {
|
||
id, err := strconv.ParseUint(c.Param("id"), 10, 32)
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
var input service.NodeUpdateInput
|
||
if err := c.ShouldBindJSON(&input); err != nil {
|
||
c.JSON(stdhttp.StatusBadRequest, gin.H{"code": "INVALID_INPUT", "message": err.Error()})
|
||
return
|
||
}
|
||
item, err := h.service.Update(c.Request.Context(), uint(id), input)
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
recordAudit(c, h.auditService, "node", "update", "node", fmt.Sprintf("%d", id), item.Name,
|
||
fmt.Sprintf("更新节点「%s」(ID: %d)", item.Name, id))
|
||
response.Success(c, item)
|
||
}
|
||
|
||
func (h *NodeHandler) Heartbeat(c *gin.Context) {
|
||
var input struct {
|
||
Token string `json:"token" binding:"required"`
|
||
Hostname string `json:"hostname"`
|
||
IPAddress string `json:"ipAddress"`
|
||
AgentVersion string `json:"agentVersion"`
|
||
OS string `json:"os"`
|
||
Arch string `json:"arch"`
|
||
}
|
||
if err := c.ShouldBindJSON(&input); err != nil {
|
||
c.JSON(stdhttp.StatusBadRequest, gin.H{"code": "INVALID_INPUT", "message": err.Error()})
|
||
return
|
||
}
|
||
if err := h.service.Heartbeat(c.Request.Context(), input.Token, input.Hostname, input.IPAddress, input.AgentVersion, input.OS, input.Arch); err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
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: h.resolveCurrentUserID(c),
|
||
})
|
||
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))
|
||
}
|