mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 05:04:22 +00:00
fix(clients): include inboundIds and traffic in /clients/list
ClientRecord got its own MarshalJSON in the previous commit, and ClientWithAttachments embeds it to add inboundIds and traffic. Go promotes the embedded MarshalJSON to the outer struct, so the encoder was calling ClientRecord.MarshalJSON for the whole value and silently dropping the extras. The frontend reads row.inboundIds / row.traffic from /clients/list, so attached inbounds didn't render and newly added clients looked like they hadn't saved. Add an explicit MarshalJSON on ClientWithAttachments that splices the extras in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1f2769cebf
commit
719fd5086d
2 changed files with 88 additions and 0 deletions
|
|
@ -24,6 +24,35 @@ type ClientWithAttachments struct {
|
||||||
Traffic *xray.ClientTraffic `json:"traffic,omitempty"`
|
Traffic *xray.ClientTraffic `json:"traffic,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarshalJSON is required because model.ClientRecord defines its own
|
||||||
|
// MarshalJSON. Go promotes the embedded method to the outer struct, so without
|
||||||
|
// this the encoder would call ClientRecord.MarshalJSON for the whole value and
|
||||||
|
// silently drop InboundIds and Traffic from the API response.
|
||||||
|
func (c ClientWithAttachments) MarshalJSON() ([]byte, error) {
|
||||||
|
rec, err := json.Marshal(c.ClientRecord)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
extras := struct {
|
||||||
|
InboundIds []int `json:"inboundIds"`
|
||||||
|
Traffic *xray.ClientTraffic `json:"traffic,omitempty"`
|
||||||
|
}{InboundIds: c.InboundIds, Traffic: c.Traffic}
|
||||||
|
extra, err := json.Marshal(extras)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(rec) < 2 || rec[len(rec)-1] != '}' || len(extra) <= 2 {
|
||||||
|
return rec, nil
|
||||||
|
}
|
||||||
|
out := make([]byte, 0, len(rec)+len(extra))
|
||||||
|
out = append(out, rec[:len(rec)-1]...)
|
||||||
|
if len(rec) > 2 {
|
||||||
|
out = append(out, ',')
|
||||||
|
}
|
||||||
|
out = append(out, extra[1:]...)
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func clientKeyForProtocol(p model.Protocol, rec *model.ClientRecord) string {
|
func clientKeyForProtocol(p model.Protocol, rec *model.ClientRecord) string {
|
||||||
if rec == nil {
|
if rec == nil {
|
||||||
return ""
|
return ""
|
||||||
|
|
|
||||||
59
web/service/client_test.go
Normal file
59
web/service/client_test.go
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/database/model"
|
||||||
|
"github.com/mhsanaei/3x-ui/v3/xray"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClientWithAttachmentsMarshalJSONIncludesExtras(t *testing.T) {
|
||||||
|
c := ClientWithAttachments{
|
||||||
|
ClientRecord: model.ClientRecord{Id: 1, Email: "alice@example.com"},
|
||||||
|
InboundIds: []int{3, 5},
|
||||||
|
Traffic: &xray.ClientTraffic{Email: "alice@example.com", Up: 1024, Down: 4096, Enable: true},
|
||||||
|
}
|
||||||
|
out, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var parsed map[string]any
|
||||||
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
||||||
|
t.Fatalf("output is not valid JSON: %v", err)
|
||||||
|
}
|
||||||
|
if parsed["email"] != "alice@example.com" {
|
||||||
|
t.Errorf("expected ClientRecord fields to survive, got %v", parsed)
|
||||||
|
}
|
||||||
|
ids, ok := parsed["inboundIds"].([]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected inboundIds to be present as an array, got %T (%s)", parsed["inboundIds"], out)
|
||||||
|
}
|
||||||
|
if len(ids) != 2 {
|
||||||
|
t.Errorf("expected 2 inbound ids, got %d", len(ids))
|
||||||
|
}
|
||||||
|
if _, ok := parsed["traffic"].(map[string]any); !ok {
|
||||||
|
t.Errorf("expected traffic to be present as an object, got %T", parsed["traffic"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClientWithAttachmentsMarshalJSONOmitsAbsentTraffic(t *testing.T) {
|
||||||
|
c := ClientWithAttachments{
|
||||||
|
ClientRecord: model.ClientRecord{Id: 1, Email: "bob@example.com"},
|
||||||
|
InboundIds: nil,
|
||||||
|
}
|
||||||
|
out, err := json.Marshal(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Marshal failed: %v", err)
|
||||||
|
}
|
||||||
|
var parsed map[string]any
|
||||||
|
if err := json.Unmarshal(out, &parsed); err != nil {
|
||||||
|
t.Fatalf("output is not valid JSON: %v", err)
|
||||||
|
}
|
||||||
|
if _, present := parsed["traffic"]; present {
|
||||||
|
t.Errorf("expected traffic to be omitted when nil, got %v", parsed["traffic"])
|
||||||
|
}
|
||||||
|
if _, present := parsed["inboundIds"]; !present {
|
||||||
|
t.Errorf("expected inboundIds key to always be present, got %s", out)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue