diff --git a/docs/Tasktracking/2026-04-26-fix-register-user-client-flow-autofill.md b/docs/Tasktracking/2026-04-26-fix-register-user-client-flow-autofill.md new file mode 100644 index 00000000..83644218 --- /dev/null +++ b/docs/Tasktracking/2026-04-26-fix-register-user-client-flow-autofill.md @@ -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. diff --git a/web/service/user.go b/web/service/user.go index a9f5c171..b094f19f 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -102,6 +102,9 @@ func (s *UserService) addUserClientsToAllInbounds(tx *gorm.DB, username string, SubID: uuid.New().String()[:8], Comment: "auto-added on registration", } + if shouldAutoFillVisionFlow(inbound.Protocol, inbound.StreamSettings) { + client.Flow = "xtls-rprx-vision" + } clientEntry := map[string]any{ "email": client.Email, @@ -122,6 +125,9 @@ func (s *UserService) addUserClientsToAllInbounds(tx *gorm.DB, username string, default: clientEntry["id"] = clientID } + if client.Flow != "" { + clientEntry["flow"] = client.Flow + } var settings map[string]any if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil { diff --git a/web/service/user_test.go b/web/service/user_test.go index 0d18efea..ea97a609 100644 --- a/web/service/user_test.go +++ b/web/service/user_test.go @@ -285,3 +285,87 @@ func TestDeleteUser_RemovesClientsFromAllInbounds(t *testing.T) { 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, "") +}