Преглед изворни кода

fix(clients): reuse stored credentials when re-adding an existing identity

Create permits a repeat add for an email that already exists when the
payload subId matches the stored one (the documented way to attach an
identity to more inbounds), but it never seeded the payload from the
existing record, so an omitted id minted a fresh UUID via
fillProtocolDefaults. SyncInbound then overwrote the shared clients.uuid
row by email while previously-attached inbounds kept the original UUID
in their settings JSON, silently desyncing panel credentials from
subscription links. BulkCreate had the identical gap.

Seed ID/Password/Auth/Secret from the existing record in both paths
(mirroring what Update, Attach and BulkAttach already do), and preserve
Secret in Update too so partial edits of MTProto clients cannot rotate
the stored secret.

Closes #5903
MHSanaei пре 1 дан
родитељ
комит
3b731cd657

+ 20 - 6
internal/web/service/client_bulk.go

@@ -1208,7 +1208,7 @@ func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []Client
 
 
 	db := database.GetDB()
 	db := database.GetDB()
 	const lookupChunk = 400
 	const lookupChunk = 400
-	existingEmailSub := make(map[string]string, len(emails))
+	existingByEmail := make(map[string]model.ClientRecord, len(emails))
 	for start := 0; start < len(emails); start += lookupChunk {
 	for start := 0; start < len(emails); start += lookupChunk {
 		end := min(start+lookupChunk, len(emails))
 		end := min(start+lookupChunk, len(emails))
 		var rows []model.ClientRecord
 		var rows []model.ClientRecord
@@ -1216,7 +1216,7 @@ func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []Client
 			return result, false, e
 			return result, false, e
 		}
 		}
 		for i := range rows {
 		for i := range rows {
-			existingEmailSub[strings.ToLower(rows[i].Email)] = rows[i].SubID
+			existingByEmail[strings.ToLower(rows[i].Email)] = rows[i]
 		}
 		}
 	}
 	}
 	existingSubOwner := make(map[string]string, len(subIDs))
 	existingSubOwner := make(map[string]string, len(subIDs))
@@ -1252,10 +1252,24 @@ func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []Client
 
 
 	for idx := range prep {
 	for idx := range prep {
 		le := strings.ToLower(prep[idx].client.Email)
 		le := strings.ToLower(prep[idx].client.Email)
-		if existSub, ok := existingEmailSub[le]; ok && existSub != prep[idx].client.SubID {
-			failed[idx] = true
-			reason[idx] = "email already in use: " + prep[idx].client.Email
-			continue
+		if rec, ok := existingByEmail[le]; ok {
+			if rec.SubID != prep[idx].client.SubID {
+				failed[idx] = true
+				reason[idx] = "email already in use: " + prep[idx].client.Email
+				continue
+			}
+			if prep[idx].client.ID == "" {
+				prep[idx].client.ID = rec.UUID
+			}
+			if prep[idx].client.Password == "" {
+				prep[idx].client.Password = rec.Password
+			}
+			if prep[idx].client.Auth == "" {
+				prep[idx].client.Auth = rec.Auth
+			}
+			if prep[idx].client.Secret == "" {
+				prep[idx].client.Secret = rec.Secret
+			}
 		}
 		}
 		if owner, ok := existingSubOwner[prep[idx].client.SubID]; ok && owner != le {
 		if owner, ok := existingSubOwner[prep[idx].client.SubID]; ok && owner != le {
 			failed[idx] = true
 			failed[idx] = true

+ 54 - 0
internal/web/service/client_create_reuse_test.go

@@ -0,0 +1,54 @@
+package service
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/mhsanaei/3x-ui/v3/internal/database/model"
+)
+
+func settingsHoldUUID(t *testing.T, inboundSvc *InboundService, inboundId int, uuid string) bool {
+	t.Helper()
+	ib, err := inboundSvc.GetInbound(inboundId)
+	if err != nil {
+		t.Fatalf("GetInbound %d: %v", inboundId, err)
+	}
+	return strings.Contains(ib.Settings, uuid)
+}
+
+func TestCreateRepeatKeepsExistingUUID(t *testing.T) {
+	setupBulkDB(t)
+	svc := &ClientService{}
+	inboundSvc := &InboundService{}
+
+	ibA := mkInbound(t, 21001, model.VLESS, `{"clients":[]}`)
+	ibB := mkInbound(t, 21002, model.VLESS, `{"clients":[]}`)
+
+	const originalUUID = "aaaaaaaa-1111-2222-3333-444444444444"
+	if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
+		Client:     model.Client{Email: "repeat@x", ID: originalUUID, SubID: "sub-repeat", Enable: true},
+		InboundIds: []int{ibA.Id},
+	}); err != nil {
+		t.Fatalf("first Create: %v", err)
+	}
+	if rec := lookupClientRecord(t, "repeat@x"); rec.UUID != originalUUID {
+		t.Fatalf("record UUID after first Create = %q, want %q", rec.UUID, originalUUID)
+	}
+
+	if _, err := svc.Create(inboundSvc, &ClientCreatePayload{
+		Client:     model.Client{Email: "repeat@x", SubID: "sub-repeat", Enable: true},
+		InboundIds: []int{ibB.Id},
+	}); err != nil {
+		t.Fatalf("repeat Create: %v", err)
+	}
+
+	if rec := lookupClientRecord(t, "repeat@x"); rec.UUID != originalUUID {
+		t.Fatalf("record UUID after repeat Create = %q, want %q", rec.UUID, originalUUID)
+	}
+	if !settingsHoldUUID(t, inboundSvc, ibA.Id, originalUUID) {
+		t.Fatalf("inbound A settings lost the original UUID")
+	}
+	if !settingsHoldUUID(t, inboundSvc, ibB.Id, originalUUID) {
+		t.Fatalf("inbound B settings did not reuse the original UUID")
+	}
+}

+ 17 - 0
internal/web/service/client_crud.go

@@ -82,6 +82,20 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
 		if existing.SubID == "" || existing.SubID != client.SubID {
 		if existing.SubID == "" || existing.SubID != client.SubID {
 			return false, common.NewError("email already in use:", client.Email)
 			return false, common.NewError("email already in use:", client.Email)
 		}
 		}
+		// Reuse stored credentials when re-adding an existing identity, or
+		// fillProtocolDefaults mints a fresh UUID that desyncs other inbounds.
+		if client.ID == "" {
+			client.ID = existing.UUID
+		}
+		if client.Password == "" {
+			client.Password = existing.Password
+		}
+		if client.Auth == "" {
+			client.Auth = existing.Auth
+		}
+		if client.Secret == "" {
+			client.Secret = existing.Secret
+		}
 	}
 	}
 
 
 	if client.SubID != "" {
 	if client.SubID != "" {
@@ -345,6 +359,9 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
 	if updated.Auth == "" {
 	if updated.Auth == "" {
 		updated.Auth = existing.Auth
 		updated.Auth = existing.Auth
 	}
 	}
+	if updated.Secret == "" {
+		updated.Secret = existing.Secret
+	}
 
 
 	if updated.Email != existing.Email {
 	if updated.Email != existing.Email {
 		var collisionCount int64
 		var collisionCount int64