mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-12 07:36:07 +00:00
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
This commit is contained in:
@@ -1208,7 +1208,7 @@ func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []Client
|
||||
|
||||
db := database.GetDB()
|
||||
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 {
|
||||
end := min(start+lookupChunk, len(emails))
|
||||
var rows []model.ClientRecord
|
||||
@@ -1216,7 +1216,7 @@ func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []Client
|
||||
return result, false, e
|
||||
}
|
||||
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))
|
||||
@@ -1252,10 +1252,24 @@ func (s *ClientService) BulkCreate(inboundSvc *InboundService, payloads []Client
|
||||
|
||||
for idx := range prep {
|
||||
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 {
|
||||
failed[idx] = true
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,20 @@ func (s *ClientService) Create(inboundSvc *InboundService, payload *ClientCreate
|
||||
if existing.SubID == "" || existing.SubID != client.SubID {
|
||||
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 != "" {
|
||||
@@ -345,6 +359,9 @@ func (s *ClientService) Update(inboundSvc *InboundService, id int, updated model
|
||||
if updated.Auth == "" {
|
||||
updated.Auth = existing.Auth
|
||||
}
|
||||
if updated.Secret == "" {
|
||||
updated.Secret = existing.Secret
|
||||
}
|
||||
|
||||
if updated.Email != existing.Email {
|
||||
var collisionCount int64
|
||||
|
||||
Reference in New Issue
Block a user