From b2efeec70ff8999adcddfbff0a72d20ca74c6293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=94=D0=BC=D0=B8=D1=82=D1=80=D0=B8=D0=B9=20=D0=A1=D0=B0?= =?UTF-8?q?=D0=B5=D0=BD=D0=BA=D0=BE?= Date: Wed, 29 Oct 2025 22:17:26 +0300 Subject: [PATCH] feat sync btn --- web/controller/multi_server_controller.go | 15 +++ web/html/servers.html | 22 +++- web/service/multi_server_service.go | 133 +++++++++++++++++++++- 3 files changed, 165 insertions(+), 5 deletions(-) diff --git a/web/controller/multi_server_controller.go b/web/controller/multi_server_controller.go index 4164febb..f10b1db9 100644 --- a/web/controller/multi_server_controller.go +++ b/web/controller/multi_server_controller.go @@ -26,6 +26,7 @@ func (c *MultiServerController) initRouter(g *gin.RouterGroup) { g.POST("/del/:id", c.delServer) g.POST("/update/:id", c.updateServer) g.GET("/onlines", c.getOnlineClients) + g.POST("/sync/:id", c.syncServer) } func (c *MultiServerController) getServers(ctx *gin.Context) { @@ -96,3 +97,17 @@ func (c *MultiServerController) updateServer(ctx *gin.Context) { } jsonMsg(ctx, "Server updated successfully", nil) } + +func (c *MultiServerController) syncServer(ctx *gin.Context) { + id, err := strconv.Atoi(ctx.Param("id")) + if err != nil { + jsonMsg(ctx, "Invalid ID", err) + return + } + err = c.multiServerService.SyncServer(id) + if err != nil { + jsonMsg(ctx, "Failed to sync server", err) + return + } + jsonMsg(ctx, "Server synced successfully", nil) +} diff --git a/web/html/servers.html b/web/html/servers.html index ad240642..9872b370 100644 --- a/web/html/servers.html +++ b/web/html/servers.html @@ -3,7 +3,6 @@ {{ template "page/head_end" .}} {{ template "page/body_start" .}} - @@ -111,9 +110,11 @@ class="badge bg-danger">No + @click="showEditModal(server)">Edit + + @@ -331,6 +332,19 @@ alert(error.response.data.msg); }); }, + syncServer(server) { + if (!confirm("Are you sure you want to sync inbounds with server \"" + server.name + "\" server?")) { + return; + } + axios + .post(`/panel/api/servers/sync/${server.id}`) + .then((response) => { + alert(response.data.msg); + }) + .catch((error) => { + alert(error.response.data.msg); + }); + }, deleteServer(id) { if (!confirm("Are you sure you want to delete this server?")) { return; diff --git a/web/service/multi_server_service.go b/web/service/multi_server_service.go index 7cb38527..4d94cbc5 100644 --- a/web/service/multi_server_service.go +++ b/web/service/multi_server_service.go @@ -1,12 +1,14 @@ package service import ( + "bytes" "encoding/json" "fmt" "net/http" "github.com/mhsanaei/3x-ui/v2/database" "github.com/mhsanaei/3x-ui/v2/database/model" + "github.com/mhsanaei/3x-ui/v2/logger" ) type MultiServerService struct{} @@ -34,7 +36,7 @@ func (s *MultiServerService) GetOnlineClients() (map[int][]string, error) { return nil, err } - clients := make( map[int][]string) + clients := make(map[int][]string) for _, server := range servers { var onlineResp struct { Success bool `json:"success"` @@ -72,3 +74,132 @@ func (s *MultiServerService) DeleteServer(id int) error { db := database.GetDB() return db.Delete(&model.Server{}, id).Error } + +// SyncServer synchronizes the inbounds list between the given server and the local inbounds list. +// It gets the inbounds list from the server, and then syncs it with the local inbounds list. +// If an inbound exists on the server but not locally, it adds the inbound. +// If an inbound exists locally but not on the server, it removes the inbound. +// If an inbound exists on both the server and locally, it updates the inbound if they are different. +func (s *MultiServerService) SyncServer(id int) error { + inboundService := &InboundService{} + inboundsSource, err := inboundService.GetAllInbounds() + if err != nil { + logger.Error("failed to get all inbounds", "err", err) + return err + } + + db := database.GetDB() + var server model.Server + if err = db.First(&server, id).Error; err != nil { + logger.Error("failed to get server", "err", err) + return err + } + + //get inbounds from server throw api + listURL := fmt.Sprintf("http://%s:%d%spanel/api/inbounds/list", server.Address, server.Port, server.SecretWebPath) + req, _ := http.NewRequest("GET", listURL, nil) + req.Header.Set("X-API-KEY", server.APIKey) + httpResp, err := http.DefaultClient.Do(req) + if err != nil { + logger.Error("failed to get inbounds from server", "err", err) + return err + } + defer httpResp.Body.Close() + + var resp struct { + Success bool `json:"success"` + Msg string `json:"msg"` + Obj []model.Inbound `json:"obj"` + } + if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { + logger.Error("failed to decode inbounds response", "err", err) + return err + } + + type InboundPayload struct { + Up int64 `json:"up"` + Down int64 `json:"down"` + Total int64 `json:"total"` + Remark string `json:"remark"` + Enable bool `json:"enable"` + ExpiryTime int64 `json:"expiryTime"` + Listen string `json:"listen"` + Port int `json:"port"` + Protocol model.Protocol `json:"protocol"` + Settings string `json:"settings"` + StreamSettings string `json:"streamSettings"` + Sniffing string `json:"sniffing"` + } + + //sync inbounds + for _, src := range inboundsSource { + logger.Debugf("syncing inbound %d", src.Id) + found := false + for _, remote := range resp.Obj { + if remote.Tag == src.Tag { + found = true + break + } + } + + payload := InboundPayload{ + Up: src.Up, Down: src.Down, Total: src.Total, + Remark: src.Remark, Enable: src.Enable, + ExpiryTime: src.ExpiryTime, Listen: src.Listen, + Port: src.Port, Protocol: src.Protocol, + Settings: src.Settings, StreamSettings: src.StreamSettings, Sniffing: src.Sniffing, + } + + data, _ := json.Marshal(payload) + + if found { + //update inbound trow api + updateURL := fmt.Sprintf("http://%s:%d%spanel/api/inbounds/update/%d", server.Address, server.Port, server.SecretWebPath, src.Id) + req, _ := http.NewRequest("POST", updateURL, bytes.NewBuffer(data)) + req.Header.Set("X-API-KEY", server.APIKey) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + logger.Error("failed to update inbounds at server", "err", err) + return err + } + var updateResp struct { + Success bool `json:"success"` + Msg string `json:"msg"` + } + if err := json.NewDecoder(httpResp.Body).Decode(&updateResp); err != nil { + logger.Error("failed to decode update inbounds response", "err", err) + return fmt.Errorf("decode update inbounds: %w", err) + } + resp.Body.Close() + if !updateResp.Success { + return fmt.Errorf("failed to update inbounds at %s %s", server.Name, server.Address) + } + } else { + // add inbound trow api + addURL := fmt.Sprintf("http://%s:%d%spanel/api/inbounds/add", server.Address, server.Port, server.SecretWebPath) + req, _ := http.NewRequest("POST", addURL, bytes.NewBuffer(data)) + req.Header.Set("X-API-KEY", server.APIKey) + req.Header.Set("Content-Type", "application/json") + resp, err := http.DefaultClient.Do(req) + if err != nil { + logger.Error("failed to add inbounds at server", "err", err) + return err + } + + var addResp struct { + Success bool `json:"success"` + Msg string `json:"msg"` + } + if err := json.NewDecoder(httpResp.Body).Decode(&addResp); err != nil { + logger.Error("failed to decode add inbounds response", "err", err) + return fmt.Errorf("decode add inbounds: %w", err) + } + resp.Body.Close() + if !addResp.Success { + return fmt.Errorf("failed to add inbounds at %s %s", server.Name, server.Address) + } + } + } + return nil +}