mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-06-07 05:34:17 +00:00
fix(user): sync-remove inbound clients when deleting managed user
This commit is contained in:
parent
dfbe02c2b8
commit
67d24ca0e6
3 changed files with 213 additions and 5 deletions
|
|
@ -105,7 +105,7 @@ func (a *UserController) deleteUser(c *gin.Context) {
|
|||
}
|
||||
|
||||
currentUser := session.GetLoginUser(c)
|
||||
err = a.userService.DeleteUser(id, currentUser.Id)
|
||||
err = a.userService.DeleteUser(id, currentUser.Id, &a.inboundService)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.users.toasts.delete"), a.localizeUserError(c, err))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -151,6 +151,68 @@ func (s *UserService) addUserClientsToAllInbounds(tx *gorm.DB, username string,
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *UserService) removeUserClientsFromAllInbounds(tx *gorm.DB, username string, inboundService *InboundService) error {
|
||||
inbounds, err := inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, inbound := range inbounds {
|
||||
var settings map[string]any
|
||||
if err := json.Unmarshal([]byte(inbound.Settings), &settings); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
clientsRaw, ok := settings["clients"].([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
newClients := make([]any, 0, len(clientsRaw))
|
||||
removedEmails := make(map[string]struct{})
|
||||
for _, clientRaw := range clientsRaw {
|
||||
clientMap, ok := clientRaw.(map[string]any)
|
||||
if !ok {
|
||||
newClients = append(newClients, clientRaw)
|
||||
continue
|
||||
}
|
||||
|
||||
email, _ := clientMap["email"].(string)
|
||||
if strings.EqualFold(email, username) {
|
||||
if email != "" {
|
||||
removedEmails[email] = struct{}{}
|
||||
}
|
||||
continue
|
||||
}
|
||||
newClients = append(newClients, clientRaw)
|
||||
}
|
||||
|
||||
if len(removedEmails) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
settings["clients"] = newClients
|
||||
newSettings, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Model(&model.Inbound{}).Where("id = ?", inbound.Id).Update("settings", string(newSettings)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for email := range removedEmails {
|
||||
if err := inboundService.DelClientStat(tx, inbound.Id, email); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := inboundService.DelClientIPs(tx, email); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFirstUser retrieves the first user from the database.
|
||||
// This is typically used for initial setup or when there's only one admin user.
|
||||
func (s *UserService) GetFirstUser() (*model.User, error) {
|
||||
|
|
@ -368,7 +430,7 @@ func (s *UserService) UpdateManagedUser(id int, username string, password string
|
|||
}
|
||||
|
||||
// DeleteUser deletes a managed user.
|
||||
func (s *UserService) DeleteUser(id int, currentUserId int) error {
|
||||
func (s *UserService) DeleteUser(id int, currentUserId int, inboundService *InboundService) error {
|
||||
if id == currentUserId {
|
||||
return ErrCannotDeleteSelf
|
||||
}
|
||||
|
|
@ -392,7 +454,9 @@ func (s *UserService) DeleteUser(id int, currentUserId int) error {
|
|||
}
|
||||
}
|
||||
|
||||
inboundService := InboundService{}
|
||||
if inboundService == nil {
|
||||
inboundService = &InboundService{}
|
||||
}
|
||||
inbounds, err := inboundService.GetInbounds(id)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -402,8 +466,12 @@ func (s *UserService) DeleteUser(id int, currentUserId int) error {
|
|||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return db.Delete(&model.User{}, id).Error
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := s.removeUserClientsFromAllInbounds(tx, user.Username, inboundService); err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.User{}, id).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (s *UserService) RegisterUser(username string, password string, inboundService *InboundService) error {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
|
@ -8,6 +9,7 @@ import (
|
|||
"github.com/mhsanaei/3x-ui/v2/database"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"github.com/mhsanaei/3x-ui/v2/util/crypto"
|
||||
"github.com/mhsanaei/3x-ui/v2/xray"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) {
|
||||
|
|
@ -145,3 +147,141 @@ func TestUpdateFirstUser_CreateWhenNone(t *testing.T) {
|
|||
t.Error("password hash should match 'firstpass'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteUser_RemovesClientsFromAllInbounds(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
|
||||
db := database.GetDB()
|
||||
userSvc := &UserService{}
|
||||
inboundSvc := &InboundService{}
|
||||
|
||||
hashedPassword, err := crypto.HashPasswordAsBcrypt("password123")
|
||||
if err != nil {
|
||||
t.Fatalf("hash password failed: %v", err)
|
||||
}
|
||||
managedUser := &model.User{
|
||||
Username: "managed_user",
|
||||
Password: hashedPassword,
|
||||
Role: "user",
|
||||
}
|
||||
if err := db.Create(managedUser).Error; err != nil {
|
||||
t.Fatalf("create managed user failed: %v", err)
|
||||
}
|
||||
|
||||
settings1Bytes, err := json.Marshal(map[string]any{
|
||||
"clients": []map[string]any{
|
||||
{"id": "client-1", "email": "managed_user", "enable": false},
|
||||
{"id": "client-2", "email": "keep_user", "enable": false},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal inbound settings 1 failed: %v", err)
|
||||
}
|
||||
settings2Bytes, err := json.Marshal(map[string]any{
|
||||
"clients": []map[string]any{
|
||||
{"id": "client-3", "email": "managed_user", "enable": false},
|
||||
{"id": "client-4", "email": "another_user", "enable": false},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("marshal inbound settings 2 failed: %v", err)
|
||||
}
|
||||
|
||||
inbound1 := &model.Inbound{
|
||||
UserId: 1,
|
||||
Port: 21001,
|
||||
Protocol: model.VLESS,
|
||||
Tag: "user-delete-sync-1",
|
||||
Settings: string(settings1Bytes),
|
||||
}
|
||||
inbound2 := &model.Inbound{
|
||||
UserId: 1,
|
||||
Port: 21002,
|
||||
Protocol: model.VLESS,
|
||||
Tag: "user-delete-sync-2",
|
||||
Settings: string(settings2Bytes),
|
||||
}
|
||||
if err := db.Create(inbound1).Error; err != nil {
|
||||
t.Fatalf("create inbound1 failed: %v", err)
|
||||
}
|
||||
if err := db.Create(inbound2).Error; err != nil {
|
||||
t.Fatalf("create inbound2 failed: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Create(&xray.ClientTraffic{InboundId: inbound1.Id, Email: "managed_user"}).Error; err != nil {
|
||||
t.Fatalf("create traffic for inbound1 failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{InboundId: inbound2.Id, Email: "managed_user"}).Error; err != nil {
|
||||
t.Fatalf("create traffic for inbound2 failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&xray.ClientTraffic{InboundId: inbound1.Id, Email: "keep_user"}).Error; err != nil {
|
||||
t.Fatalf("create keep_user traffic failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.InboundClientIps{ClientEmail: "managed_user", Ips: "[\"1.1.1.1\"]"}).Error; err != nil {
|
||||
t.Fatalf("create inbound client ips failed: %v", err)
|
||||
}
|
||||
|
||||
if err := userSvc.DeleteUser(managedUser.Id, 1, inboundSvc); err != nil {
|
||||
t.Fatalf("DeleteUser failed: %v", err)
|
||||
}
|
||||
|
||||
var usersCount int64
|
||||
if err := db.Model(&model.User{}).Where("id = ?", managedUser.Id).Count(&usersCount).Error; err != nil {
|
||||
t.Fatalf("count users failed: %v", err)
|
||||
}
|
||||
if usersCount != 0 {
|
||||
t.Fatalf("expected managed user to be deleted, remaining=%d", usersCount)
|
||||
}
|
||||
|
||||
checkInboundHasNoManagedUser := func(inboundID int) {
|
||||
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 == "managed_user" {
|
||||
t.Fatalf("managed_user still exists in inbound %d clients", inboundID)
|
||||
}
|
||||
}
|
||||
}
|
||||
checkInboundHasNoManagedUser(inbound1.Id)
|
||||
checkInboundHasNoManagedUser(inbound2.Id)
|
||||
|
||||
var managedTrafficCount int64
|
||||
if err := db.Model(&xray.ClientTraffic{}).Where("email = ?", "managed_user").Count(&managedTrafficCount).Error; err != nil {
|
||||
t.Fatalf("count managed user traffic failed: %v", err)
|
||||
}
|
||||
if managedTrafficCount != 0 {
|
||||
t.Fatalf("expected managed_user traffic to be deleted, remaining=%d", managedTrafficCount)
|
||||
}
|
||||
|
||||
var keepTrafficCount int64
|
||||
if err := db.Model(&xray.ClientTraffic{}).Where("email = ?", "keep_user").Count(&keepTrafficCount).Error; err != nil {
|
||||
t.Fatalf("count keep_user traffic failed: %v", err)
|
||||
}
|
||||
if keepTrafficCount != 1 {
|
||||
t.Fatalf("expected keep_user traffic to remain, got=%d", keepTrafficCount)
|
||||
}
|
||||
|
||||
var ipsCount int64
|
||||
if err := db.Model(&model.InboundClientIps{}).Where("client_email = ?", "managed_user").Count(&ipsCount).Error; err != nil {
|
||||
t.Fatalf("count inbound client ips failed: %v", err)
|
||||
}
|
||||
if ipsCount != 0 {
|
||||
t.Fatalf("expected managed_user inbound_client_ips to be deleted, remaining=%d", ipsCount)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue