diff --git a/web/service/client.go b/web/service/client.go index a464029e..b25854d6 100644 --- a/web/service/client.go +++ b/web/service/client.go @@ -213,12 +213,24 @@ func (s *ClientService) SyncInbound(tx *gorm.DB, inboundId int, clients []model. } row = incoming } else { - row.UUID = incoming.UUID - row.Password = incoming.Password - row.Auth = incoming.Auth - row.Flow = incoming.Flow - row.Security = incoming.Security - row.Reverse = incoming.Reverse + if incoming.UUID != "" { + row.UUID = incoming.UUID + } + if incoming.Password != "" { + row.Password = incoming.Password + } + if incoming.Auth != "" { + row.Auth = incoming.Auth + } + if incoming.Flow != "" { + row.Flow = incoming.Flow + } + if incoming.Security != "" { + row.Security = incoming.Security + } + if incoming.Reverse != "" { + row.Reverse = incoming.Reverse + } row.SubID = incoming.SubID row.LimitIP = incoming.LimitIP row.TotalGB = incoming.TotalGB diff --git a/web/service/client_sync_multiprotocol_test.go b/web/service/client_sync_multiprotocol_test.go new file mode 100644 index 00000000..335c7e82 --- /dev/null +++ b/web/service/client_sync_multiprotocol_test.go @@ -0,0 +1,58 @@ +package service + +import ( + "path/filepath" + "testing" + + "github.com/mhsanaei/3x-ui/v3/database" + "github.com/mhsanaei/3x-ui/v3/database/model" +) + +func TestSyncInbound_PreservesCredentialsAcrossProtocols(t *testing.T) { + dbDir := t.TempDir() + t.Setenv("XUI_DB_FOLDER", dbDir) + if err := database.InitDB(filepath.Join(dbDir, "3x-ui.db")); err != nil { + t.Fatalf("InitDB: %v", err) + } + t.Cleanup(func() { _ = database.CloseDB() }) + + db := database.GetDB() + + vlessInbound := &model.Inbound{Tag: "vless-in", Enable: true, Port: 10001, Protocol: model.VLESS} + if err := db.Create(vlessInbound).Error; err != nil { + t.Fatalf("create vless inbound: %v", err) + } + hysteriaInbound := &model.Inbound{Tag: "hy-in", Enable: true, Port: 10002, Protocol: model.Hysteria2} + if err := db.Create(hysteriaInbound).Error; err != nil { + t.Fatalf("create hysteria inbound: %v", err) + } + + svc := ClientService{} + const sharedEmail = "shared@example.com" + const wantUUID = "ce8d33df-3a64-4f10-8f9b-91c3a8e0c001" + const wantAuth = "h2-auth-token" + + vlessClient := model.Client{Email: sharedEmail, ID: wantUUID, Enable: true, Flow: "xtls-rprx-vision"} + if err := svc.SyncInbound(nil, vlessInbound.Id, []model.Client{vlessClient}); err != nil { + t.Fatalf("vless SyncInbound: %v", err) + } + + hysteriaClient := model.Client{Email: sharedEmail, Auth: wantAuth, Enable: true} + if err := svc.SyncInbound(nil, hysteriaInbound.Id, []model.Client{hysteriaClient}); err != nil { + t.Fatalf("hysteria SyncInbound: %v", err) + } + + var row model.ClientRecord + if err := db.Where("email = ?", sharedEmail).First(&row).Error; err != nil { + t.Fatalf("lookup client row: %v", err) + } + if row.UUID != wantUUID { + t.Errorf("UUID was clobbered by Hysteria sync: got %q, want %q", row.UUID, wantUUID) + } + if row.Auth != wantAuth { + t.Errorf("Auth not persisted: got %q, want %q", row.Auth, wantAuth) + } + if row.Flow != "xtls-rprx-vision" { + t.Errorf("Flow was clobbered by Hysteria sync: got %q, want xtls-rprx-vision", row.Flow) + } +}