mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-05 12:44:22 +00:00
fix(multi-node): scope remote client update/delete to one inbound (#4892)
UpdateUser and DeleteUser hit the node's email-based full-client endpoints, which fanned out to every inbound the client had on the node: editing a client wiped flow on the node's other inbounds, and detaching one node inbound deleted the client from all of them. Make both inbound-scoped, mirroring AddClient. DeleteUser now detaches the resolved remote inbound id; UpdateUser passes an inboundIds scope so the node updates only that inbound.
This commit is contained in:
parent
a07c7b7f4e
commit
db86007ab8
3 changed files with 51 additions and 13 deletions
|
|
@ -3,6 +3,8 @@ package controller
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v3/database/model"
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||||
|
|
@ -16,6 +18,21 @@ func notifyClientsChanged() {
|
||||||
websocket.BroadcastInvalidate(websocket.MessageTypeClients)
|
websocket.BroadcastInvalidate(websocket.MessageTypeClients)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func parseInboundIdsQuery(raw string) []int {
|
||||||
|
raw = strings.TrimSpace(raw)
|
||||||
|
if raw == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts := strings.Split(raw, ",")
|
||||||
|
ids := make([]int, 0, len(parts))
|
||||||
|
for _, p := range parts {
|
||||||
|
if id, err := strconv.Atoi(strings.TrimSpace(p)); err == nil {
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
|
||||||
type ClientController struct {
|
type ClientController struct {
|
||||||
clientService service.ClientService
|
clientService service.ClientService
|
||||||
inboundService service.InboundService
|
inboundService service.InboundService
|
||||||
|
|
@ -129,7 +146,8 @@ func (a *ClientController) update(c *gin.Context) {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
needRestart, err := a.clientService.UpdateByEmail(&a.inboundService, email, updated)
|
inboundFilter := parseInboundIdsQuery(c.Query("inboundIds"))
|
||||||
|
needRestart, err := a.clientService.UpdateByEmail(&a.inboundService, email, updated, inboundFilter...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -286,15 +286,17 @@ func (r *Remote) AddClient(ctx context.Context, ib *model.Inbound, client model.
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteUser is idempotent: master's per-inbound Delete loop may call it
|
func (r *Remote) DeleteUser(ctx context.Context, ib *model.Inbound, email string) error {
|
||||||
// multiple times for the same node, and "not found" on the follow-ups is
|
|
||||||
// the expected success path.
|
|
||||||
func (r *Remote) DeleteUser(ctx context.Context, _ *model.Inbound, email string) error {
|
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
_, err := r.do(ctx, http.MethodPost,
|
id, err := r.resolveRemoteID(ctx, ib.Tag)
|
||||||
"panel/api/clients/del/"+url.PathEscape(email), nil)
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
body := map[string]any{"inboundIds": []int{id}}
|
||||||
|
_, err = r.do(ctx, http.MethodPost,
|
||||||
|
"panel/api/clients/"+url.PathEscape(email)+"/detach", body)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
@ -304,12 +306,17 @@ func (r *Remote) DeleteUser(ctx context.Context, _ *model.Inbound, email string)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Remote) UpdateUser(ctx context.Context, _ *model.Inbound, oldEmail string, payload model.Client) error {
|
func (r *Remote) UpdateUser(ctx context.Context, ib *model.Inbound, oldEmail string, payload model.Client) error {
|
||||||
if oldEmail == "" {
|
if oldEmail == "" {
|
||||||
oldEmail = payload.Email
|
oldEmail = payload.Email
|
||||||
}
|
}
|
||||||
if _, err := r.do(ctx, http.MethodPost,
|
id, err := r.resolveRemoteID(ctx, ib.Tag)
|
||||||
"panel/api/clients/update/"+url.PathEscape(oldEmail), payload); err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
path := "panel/api/clients/update/" + url.PathEscape(oldEmail) +
|
||||||
|
"?inboundIds=" + strconv.Itoa(id)
|
||||||
|
if _, err := r.do(ctx, http.MethodPost, path, payload); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
|
||||||
|
|
@ -634,7 +634,7 @@ func applyShadowsocksClientMethod(clients []any, settings map[string]any) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model.Client) (bool, error) {
|
func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model.Client, inboundFilter ...int) (bool, error) {
|
||||||
existing, err := s.GetByID(id)
|
existing, err := s.GetByID(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
|
|
@ -643,6 +643,19 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
if len(inboundFilter) > 0 {
|
||||||
|
allow := make(map[int]struct{}, len(inboundFilter))
|
||||||
|
for _, fid := range inboundFilter {
|
||||||
|
allow[fid] = struct{}{}
|
||||||
|
}
|
||||||
|
filtered := inboundIds[:0:0]
|
||||||
|
for _, ibId := range inboundIds {
|
||||||
|
if _, ok := allow[ibId]; ok {
|
||||||
|
filtered = append(filtered, ibId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inboundIds = filtered
|
||||||
|
}
|
||||||
|
|
||||||
if strings.TrimSpace(updated.Email) == "" {
|
if strings.TrimSpace(updated.Email) == "" {
|
||||||
return false, common.NewError("client email is required")
|
return false, common.NewError("client email is required")
|
||||||
|
|
@ -1317,7 +1330,7 @@ func (s *ClientService) findInboundIdsByClientEmail(email string) ([]int, error)
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) UpdateByEmail(inboundSvc *InboundService, email string, updated model.Client) (bool, error) {
|
func (s *ClientService) UpdateByEmail(inboundSvc *InboundService, email string, updated model.Client, inboundFilter ...int) (bool, error) {
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return false, common.NewError("client email is required")
|
return false, common.NewError("client email is required")
|
||||||
}
|
}
|
||||||
|
|
@ -1325,7 +1338,7 @@ func (s *ClientService) UpdateByEmail(inboundSvc *InboundService, email string,
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
return s.Update(inboundSvc, rec.Id, updated)
|
return s.Update(inboundSvc, rec.Id, updated, inboundFilter...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email string) (bool, error) {
|
func (s *ClientService) ResetTrafficByEmail(inboundSvc *InboundService, email string) (bool, error) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue