mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-29 06:31:58 +00:00
fix(inbounds): keep stored client lifecycle and counters on inbound save
Invariant: saving an inbound's configuration never changes a client's enable, expiry, quota or renewal state, nor the inbound's own traffic counters. The inbound modal posts back the whole settings.clients list it loaded when it opened, and UpdateInbound stored it, synced it into the client records and wrote it into client_traffics. Any client renewed, depleted or reset while the modal was open was reverted - a renewed client came back disabled with its old expiry. The row itself was read before the serialized tx and saved whole, so traffic the poll added in between was rolled back and a depleted inbound could be re-enabled. UpdateInbound now re-reads the row inside the writer and, for clients the inbound already holds, keeps enable, expiryTime, totalGB, reset, resetDay, resetWeekday and resetMax from it; those change through the client endpoints. A master's node-sync push stays authoritative. Existing clients' lifecycle fields are no longer editable through the inbound JSON editor or /inbounds/update, which the API docs now state.
This commit is contained in:
1 parent
17d7dd46b5
commit
1110caaa65
7 files changed
+164
-11
No files matched your search
@@ -0,0 +1,94 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/database"
|
||||
"github.com/mhsanaei/3x-ui/v3/internal/xray"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func readClientTraffic(t *testing.T, email string) xray.ClientTraffic {
|
||||
t.Helper()
|
||||
var row xray.ClientTraffic
|
||||
if err := database.GetDB().Where("email = ?", email).First(&row).Error; err != nil {
|
||||
t.Fatalf("read client_traffics %s: %v", email, err)
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
// The inbound modal posts the clients it loaded on open; a renewal committed
|
||||
// while it was open must survive the save in settings, record and stats.
|
||||
func TestInboundFormSaveKeepsClientRenewedWhileOpen(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
ib := seedRenewableNeighbour(t, 23201, nil)
|
||||
form := *ib
|
||||
|
||||
if err := database.GetDB().Transaction(autoRenewTick); err != nil {
|
||||
t.Fatalf("autoRenew: %v", err)
|
||||
}
|
||||
form.Remark = "edited"
|
||||
if _, _, err := (&InboundService{}).UpdateInbound(&form); err != nil {
|
||||
t.Fatalf("UpdateInbound: %v", err)
|
||||
}
|
||||
|
||||
requireNeighbourRenewed(t, ib.Id)
|
||||
now := time.Now().UnixMilli()
|
||||
if row := readClientTraffic(t, "y@stale"); !row.Enable || row.ExpiryTime <= now {
|
||||
t.Fatalf("client_traffics rolled back: enable=%v expiryTime=%d", row.Enable, row.ExpiryTime)
|
||||
}
|
||||
if rec := lookupClientRecord(t, "y@stale"); !rec.Enable || rec.ExpiryTime <= now {
|
||||
t.Fatalf("client record rolled back: enable=%v expiryTime=%d", rec.Enable, rec.ExpiryTime)
|
||||
}
|
||||
}
|
||||
|
||||
// On a node the master's push is authoritative, lifecycle fields included.
|
||||
func TestInboundUpdateFromMasterAppliesClientLifecycle(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
ib := seedRenewableNeighbour(t, 23202, nil)
|
||||
clients, err := (&InboundService{}).GetClients(ib)
|
||||
if err != nil {
|
||||
t.Fatalf("GetClients: %v", err)
|
||||
}
|
||||
for i := range clients {
|
||||
if clients[i].Email == "x@stale" {
|
||||
clients[i].Enable = false
|
||||
}
|
||||
}
|
||||
push := *ib
|
||||
push.Settings = clientsSettings(t, clients)
|
||||
if _, _, err := (&InboundService{FromNodeSync: true}).UpdateInbound(&push); err != nil {
|
||||
t.Fatalf("UpdateInbound: %v", err)
|
||||
}
|
||||
if x, _ := settingsClient(t, ib.Id, "x@stale"); x.Enable {
|
||||
t.Fatal("master push disabling x@stale was ignored in settings")
|
||||
}
|
||||
if readClientTraffic(t, "x@stale").Enable {
|
||||
t.Fatal("master push disabling x@stale was ignored in client_traffics")
|
||||
}
|
||||
}
|
||||
|
||||
// Traffic the poll adds after UpdateInbound read the row must not be written
|
||||
// back over by the edit.
|
||||
func TestInboundUpdateKeepsTrafficAddedMidEdit(t *testing.T) {
|
||||
setupBulkDB(t)
|
||||
ib := seedRenewableNeighbour(t, 23203, nil)
|
||||
form := *ib
|
||||
form.Remark = "edited"
|
||||
commitTickBetweenReadAndWrite(t, func(tx *gorm.DB) error {
|
||||
return (&InboundService{}).addInboundTraffic(tx, []*xray.Traffic{{IsInbound: true, Tag: ib.Tag, Up: 100, Down: 50}})
|
||||
}, func() {
|
||||
if _, _, err := (&InboundService{}).UpdateInbound(&form); err != nil {
|
||||
t.Errorf("UpdateInbound: %v", err)
|
||||
}
|
||||
})
|
||||
saved, err := (&InboundService{}).GetInbound(ib.Id)
|
||||
if err != nil {
|
||||
t.Fatalf("GetInbound: %v", err)
|
||||
}
|
||||
if saved.Up != 100 || saved.Down != 50 || saved.Remark != "edited" {
|
||||
t.Fatalf("inbound after edit: up=%d down=%d remark=%q, want 100/50/edited", saved.Up, saved.Down, saved.Remark)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user