fix: auto-fill flow for registration-created eligible clients

This commit is contained in:
root 2026-04-26 01:08:22 +08:00
parent b47bac3dc6
commit 7ff73313a9
3 changed files with 144 additions and 0 deletions

View file

@ -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.

View file

@ -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 {

View file

@ -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, "")
}