218 lines
5.8 KiB
Go
218 lines
5.8 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"slices"
|
|
"strconv"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/model"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/logger"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/middleware"
|
|
"github.com/mhsanaei/3x-ui/v3/internal/web/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type NodeController struct {
|
|
nodeService service.NodeService
|
|
xrayService service.XrayService
|
|
}
|
|
|
|
func NewNodeController(g *gin.RouterGroup) *NodeController {
|
|
a := &NodeController{}
|
|
a.initRouter(g)
|
|
return a
|
|
}
|
|
|
|
func (a *NodeController) initRouter(g *gin.RouterGroup) {
|
|
g.GET("/list", a.list)
|
|
g.GET("/get/:id", a.get)
|
|
|
|
g.POST("/add", a.add)
|
|
g.POST("/update/:id", a.update)
|
|
g.POST("/del/:id", a.del)
|
|
g.POST("/setEnable/:id", a.setEnable)
|
|
|
|
g.POST("/probe/:id", a.probe)
|
|
g.POST("/updatePanel", a.updatePanel)
|
|
g.GET("/history/:id/:metric/:bucket", a.history)
|
|
}
|
|
|
|
func (a *NodeController) list(c *gin.Context) {
|
|
nodes, err := a.nodeService.GetNodeTreeView()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.list"), err)
|
|
return
|
|
}
|
|
jsonObj(c, nodes, nil)
|
|
}
|
|
|
|
func (a *NodeController) get(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
n, err := a.nodeService.GetViewById(id)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.obtain"), err)
|
|
return
|
|
}
|
|
jsonObj(c, n, nil)
|
|
}
|
|
|
|
func (a *NodeController) add(c *gin.Context) {
|
|
n, ok := middleware.BindAndValidate[service.NodeMutationRequest](c)
|
|
if !ok {
|
|
return
|
|
}
|
|
view, err := a.nodeService.CreateFromRequest(n)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.add"), err)
|
|
return
|
|
}
|
|
// Every node is agent-managed: issue an agent token bound to the
|
|
// new node and return it so the UI can display it (shown only once).
|
|
view.NodeType = "agent"
|
|
nodeID := view.Id
|
|
token, agentID, err := issueAgentToken(&nodeID, n.Name)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.add"), err)
|
|
return
|
|
}
|
|
// Probe-only (V2.1.0): preset the flag on the freshly issued
|
|
// agent row when the create form asked for it, so the very
|
|
// first config poll already serves no core payload. A failed
|
|
// preset only logs — the node and token are usable, and the
|
|
// flag can still be flipped from the edit form.
|
|
if n.ProbeOnly != nil && *n.ProbeOnly {
|
|
if err := database.GetDB().Model(&model.AgentNode{}).
|
|
Where("id = ?", agentID).
|
|
Update("probe_only", true).Error; err != nil {
|
|
logger.Warningf("node add: preset probe_only for agent %d failed: %v", agentID, err)
|
|
} else {
|
|
view.ProbeOnly = true
|
|
}
|
|
}
|
|
jsonMsgObj(c, I18nWeb(c, "pages.nodes.toasts.add"), gin.H{
|
|
"node": view,
|
|
"agentToken": token,
|
|
"nodeType": "agent",
|
|
}, nil)
|
|
}
|
|
|
|
func (a *NodeController) update(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
n, ok := middleware.BindAndValidate[service.NodeMutationRequest](c)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := a.nodeService.UpdateFromRequest(id, n); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), nil)
|
|
}
|
|
|
|
func (a *NodeController) del(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
if err := a.nodeService.Delete(id); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.delete"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.delete"), nil)
|
|
}
|
|
|
|
func (a *NodeController) setEnable(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
body := struct {
|
|
Enable bool `json:"enable" form:"enable"`
|
|
}{}
|
|
if err := c.ShouldBind(&body); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), err)
|
|
return
|
|
}
|
|
if err := a.nodeService.SetEnable(id, body.Enable); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), err)
|
|
return
|
|
}
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.update"), nil)
|
|
}
|
|
|
|
func (a *NodeController) probe(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
n, err := a.nodeService.GetById(id)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.obtain"), err)
|
|
return
|
|
}
|
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 6*time.Second)
|
|
defer cancel()
|
|
patch, probeErr := a.nodeService.Probe(ctx, n)
|
|
if probeErr != nil {
|
|
patch.Status = "offline"
|
|
} else {
|
|
patch.Status = "online"
|
|
}
|
|
if err := a.nodeService.UpdateHeartbeat(id, patch); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.nodes.toasts.test"), err)
|
|
return
|
|
}
|
|
jsonObj(c, patch.ToUI(probeErr == nil), nil)
|
|
}
|
|
|
|
func (a *NodeController) updatePanel(c *gin.Context) {
|
|
var req struct {
|
|
Ids []int `json:"ids"`
|
|
Dev bool `json:"dev"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
|
return
|
|
}
|
|
if len(req.Ids) == 0 {
|
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), fmt.Errorf("no nodes selected"))
|
|
return
|
|
}
|
|
results, err := a.nodeService.UpdatePanels(req.Ids, req.Dev)
|
|
jsonMsgObj(c, I18nWeb(c, "pages.nodes.toasts.updateStarted"), results, err)
|
|
}
|
|
|
|
func (a *NodeController) history(c *gin.Context) {
|
|
id, err := strconv.Atoi(c.Param("id"))
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "get"), err)
|
|
return
|
|
}
|
|
metric := c.Param("metric")
|
|
if !slices.Contains(service.NodeMetricKeys, metric) {
|
|
jsonMsg(c, "invalid metric", fmt.Errorf("unknown metric"))
|
|
return
|
|
}
|
|
bucket, err := strconv.Atoi(c.Param("bucket"))
|
|
if err != nil || bucket <= 0 || !service.IsAllowedHistoryBucket(bucket) {
|
|
jsonMsg(c, "invalid bucket", fmt.Errorf("unsupported bucket"))
|
|
return
|
|
}
|
|
jsonObj(c, a.nodeService.AggregateNodeMetric(id, metric, bucket, 60), nil)
|
|
}
|