mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-06 21:24:10 +00:00
fix: auto-fill flow for registration-created eligible clients
This commit is contained in:
parent
b47bac3dc6
commit
7ff73313a9
3 changed files with 144 additions and 0 deletions
|
|
@ -0,0 +1,54 @@
|
||||||
|
Task Record
|
||||||
|
|
||||||
|
Date: 2026-04-26
|
||||||
|
Related Module: web/service user registration client auto-provisioning
|
||||||
|
Change Type: Fix
|
||||||
|
|
||||||
|
Background
|
||||||
|
|
||||||
|
New user registration auto-creates clients across inbounds via `addUserClientsToAllInbounds`.
|
||||||
|
This path bypassed the previously added AddInboundClient flow auto-fill logic, so newly registered users could still get empty `flow` in eligible VLESS contexts.
|
||||||
|
|
||||||
|
Changes
|
||||||
|
|
||||||
|
Updated registration auto-provisioning path in `web/service/user.go`:
|
||||||
|
- When target inbound requires flow (`VLESS + TCP + TLS/Reality`), set client `Flow` to `xtls-rprx-vision`.
|
||||||
|
- Persist `flow` field in generated client entry when populated.
|
||||||
|
|
||||||
|
Added test in `web/service/user_test.go`:
|
||||||
|
- `TestRegisterUser_AutoFillFlowForEligibleVlessInbound`
|
||||||
|
- Verifies registered user gets `xtls-rprx-vision` flow in eligible VLESS inbound.
|
||||||
|
- Verifies non-VLESS inbound does not get forced flow.
|
||||||
|
|
||||||
|
Impact
|
||||||
|
|
||||||
|
Affected modules or files.
|
||||||
|
- `web/service/user.go`
|
||||||
|
- `web/service/user_test.go`
|
||||||
|
|
||||||
|
Whether APIs, database, config, build, or compatibility are affected.
|
||||||
|
- API unchanged.
|
||||||
|
- DB schema unchanged.
|
||||||
|
- Runtime behavior fixed for registration-created clients only.
|
||||||
|
|
||||||
|
Whether upstream or downstream callers are affected.
|
||||||
|
- Newly registered users now receive expected default flow in eligible VLESS inbounds.
|
||||||
|
|
||||||
|
Verification
|
||||||
|
|
||||||
|
List validation commands or checks performed.
|
||||||
|
- `go test ./web/service/...`
|
||||||
|
|
||||||
|
State the result.
|
||||||
|
- Passed.
|
||||||
|
|
||||||
|
If not verified, explain why.
|
||||||
|
- No remote runtime verification in deployed environment was performed locally.
|
||||||
|
|
||||||
|
Risks And Follow-Up
|
||||||
|
|
||||||
|
Remaining risks.
|
||||||
|
- Existing already-created clients are unaffected (no migration applied).
|
||||||
|
|
||||||
|
Recommended follow-up work.
|
||||||
|
- If needed, add a one-time migration tool to backfill empty flow for existing eligible clients.
|
||||||
|
|
@ -102,6 +102,9 @@ func (s *UserService) addUserClientsToAllInbounds(tx *gorm.DB, username string,
|
||||||
SubID: uuid.New().String()[:8],
|
SubID: uuid.New().String()[:8],
|
||||||
Comment: "auto-added on registration",
|
Comment: "auto-added on registration",
|
||||||
}
|
}
|
||||||
|
if shouldAutoFillVisionFlow(inbound.Protocol, inbound.StreamSettings) {
|
||||||
|
client.Flow = "xtls-rprx-vision"
|
||||||
|
}
|
||||||
|
|
||||||
clientEntry := map[string]any{
|
clientEntry := map[string]any{
|
||||||
"email": client.Email,
|
"email": client.Email,
|
||||||
|
|
@ -122,6 +125,9 @@ func (s *UserService) addUserClientsToAllInbounds(tx *gorm.DB, username string,
|
||||||
default:
|
default:
|
||||||
clientEntry["id"] = clientID
|
clientEntry["id"] = clientID
|
||||||
}
|
}
|
||||||
|
if client.Flow != "" {
|
||||||
|
clientEntry["flow"] = client.Flow
|
||||||
|
}
|
||||||
|
|
||||||
var settings map[string]any
|
var settings map[string]any
|
||||||
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
||||||
|
|
|
||||||
|
|
@ -285,3 +285,87 @@ func TestDeleteUser_RemovesClientsFromAllInbounds(t *testing.T) {
|
||||||
t.Fatalf("expected managed_user inbound_client_ips to be deleted, remaining=%d", ipsCount)
|
t.Fatalf("expected managed_user inbound_client_ips to be deleted, remaining=%d", ipsCount)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRegisterUser_AutoFillFlowForEligibleVlessInbound(t *testing.T) {
|
||||||
|
setupTestDB(t)
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
userSvc := &UserService{}
|
||||||
|
inboundSvc := &InboundService{}
|
||||||
|
|
||||||
|
vlessSettingsBytes, err := json.Marshal(map[string]any{
|
||||||
|
"clients": []map[string]any{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal vless settings failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vlessInbound := &model.Inbound{
|
||||||
|
UserId: 1,
|
||||||
|
Port: 21011,
|
||||||
|
Protocol: model.VLESS,
|
||||||
|
Tag: "register-flow-vless",
|
||||||
|
Settings: string(vlessSettingsBytes),
|
||||||
|
StreamSettings: `{"network":"tcp","security":"tls"}`,
|
||||||
|
}
|
||||||
|
if err := db.Create(vlessInbound).Error; err != nil {
|
||||||
|
t.Fatalf("create vless inbound failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
vmessSettingsBytes, err := json.Marshal(map[string]any{
|
||||||
|
"clients": []map[string]any{},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("marshal vmess settings failed: %v", err)
|
||||||
|
}
|
||||||
|
vmessInbound := &model.Inbound{
|
||||||
|
UserId: 1,
|
||||||
|
Port: 21012,
|
||||||
|
Protocol: model.VMESS,
|
||||||
|
Tag: "register-flow-vmess",
|
||||||
|
Settings: string(vmessSettingsBytes),
|
||||||
|
StreamSettings: `{"network":"tcp","security":"tls"}`,
|
||||||
|
}
|
||||||
|
if err := db.Create(vmessInbound).Error; err != nil {
|
||||||
|
t.Fatalf("create vmess inbound failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := userSvc.RegisterUser("flow_user", "password123", inboundSvc); err != nil {
|
||||||
|
t.Fatalf("RegisterUser failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertClientFlow := func(inboundID int, expectedFlow string) {
|
||||||
|
t.Helper()
|
||||||
|
var inbound model.Inbound
|
||||||
|
if err := db.First(&inbound, inboundID).Error; err != nil {
|
||||||
|
t.Fatalf("load inbound %d failed: %v", inboundID, err)
|
||||||
|
}
|
||||||
|
var settings map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
||||||
|
t.Fatalf("unmarshal inbound settings failed: %v", err)
|
||||||
|
}
|
||||||
|
clients, ok := settings["clients"].([]any)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("invalid clients format in inbound %d", inboundID)
|
||||||
|
}
|
||||||
|
for _, clientRaw := range clients {
|
||||||
|
clientMap, ok := clientRaw.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
email, _ := clientMap["email"].(string)
|
||||||
|
if email != "flow_user" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
flow, _ := clientMap["flow"].(string)
|
||||||
|
if flow != expectedFlow {
|
||||||
|
t.Fatalf("unexpected flow for inbound %d: expected %q, got %q", inboundID, expectedFlow, flow)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
t.Fatalf("flow_user not found in inbound %d", inboundID)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertClientFlow(vlessInbound.Id, "xtls-rprx-vision")
|
||||||
|
assertClientFlow(vmessInbound.Id, "")
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue