mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-07-17 01:56:06 +00:00
feat(inbounds): bulk-attach & assign-group client actions + form defaults
- Bulk-attach an inbound's clients onto other inbounds (same identity, shared traffic): new ClientService.BulkAttach + POST /clients/bulkAttach, an inbound row action, and AttachClientsModal. - Assign all of an inbound's clients to a group from the inbound page, reusing /clients/bulkAssignGroup and the existing BulkAssignGroupModal. - Default a random user/pass account for new Mixed and HTTP inbounds instead of an empty accounts list. - Capitalize the inbound Security toggle labels (None/TLS/Reality).
This commit is contained in:
@@ -48,6 +48,7 @@ func (a *ClientController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/bulkDel", a.bulkDelete)
|
||||
g.POST("/bulkCreate", a.bulkCreate)
|
||||
g.POST("/bulkAssignGroup", a.bulkAssignGroup)
|
||||
g.POST("/bulkAttach", a.bulkAttach)
|
||||
g.POST("/resetTraffic/:email", a.resetTrafficByEmail)
|
||||
g.POST("/updateTraffic/:email", a.updateTrafficByEmail)
|
||||
g.POST("/ips/:email", a.getIps)
|
||||
@@ -239,6 +240,29 @@ func (a *ClientController) bulkAssignGroup(c *gin.Context) {
|
||||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
type bulkAttachRequest struct {
|
||||
Emails []string `json:"emails"`
|
||||
InboundIds []int `json:"inboundIds"`
|
||||
}
|
||||
|
||||
func (a *ClientController) bulkAttach(c *gin.Context) {
|
||||
var req bulkAttachRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
result, needRestart, err := a.clientService.BulkAttach(&a.inboundService, req.Emails, req.InboundIds)
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, result, nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
notifyClientsChanged()
|
||||
}
|
||||
|
||||
func (a *ClientController) bulkDelete(c *gin.Context) {
|
||||
var req bulkDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
|
||||
@@ -791,6 +791,99 @@ func (s *ClientService) AttachByEmail(inboundSvc *InboundService, email string,
|
||||
return s.Attach(inboundSvc, rec.Id, inboundIds)
|
||||
}
|
||||
|
||||
// BulkAttachResult reports the outcome of a bulk attach across target inbounds.
|
||||
type BulkAttachResult struct {
|
||||
Attached []string `json:"attached"`
|
||||
Skipped []string `json:"skipped"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
// BulkAttach attaches the given existing clients (by email) to each target inbound,
|
||||
// reusing their identity (email/UUID/password/subId) and a shared traffic row. It adds
|
||||
// all clients to a target in a single AddInboundClient call, and reports clients already
|
||||
// present on a target as skipped.
|
||||
func (s *ClientService) BulkAttach(inboundSvc *InboundService, emails []string, inboundIds []int) (*BulkAttachResult, bool, error) {
|
||||
result := &BulkAttachResult{}
|
||||
if len(emails) == 0 || len(inboundIds) == 0 {
|
||||
return result, false, nil
|
||||
}
|
||||
|
||||
records := make([]*model.ClientRecord, 0, len(emails))
|
||||
seenEmail := make(map[string]struct{}, len(emails))
|
||||
for _, email := range emails {
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(email)
|
||||
if _, ok := seenEmail[key]; ok {
|
||||
continue
|
||||
}
|
||||
seenEmail[key] = struct{}{}
|
||||
rec, err := s.GetRecordByEmail(nil, email)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", email, err))
|
||||
continue
|
||||
}
|
||||
records = append(records, rec)
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
for _, ibId := range inboundIds {
|
||||
inbound, err := inboundSvc.GetInbound(ibId)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
|
||||
continue
|
||||
}
|
||||
existingClients, err := inboundSvc.GetClients(inbound)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
|
||||
continue
|
||||
}
|
||||
have := make(map[string]struct{}, len(existingClients))
|
||||
for _, c := range existingClients {
|
||||
have[strings.ToLower(c.Email)] = struct{}{}
|
||||
}
|
||||
|
||||
clientsToAdd := make([]model.Client, 0, len(records))
|
||||
for _, rec := range records {
|
||||
if _, attached := have[strings.ToLower(rec.Email)]; attached {
|
||||
result.Skipped = append(result.Skipped, rec.Email)
|
||||
continue
|
||||
}
|
||||
client := *rec.ToClient()
|
||||
client.UpdatedAt = time.Now().UnixMilli()
|
||||
if err := s.fillProtocolDefaults(&client, inbound); err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s -> inbound %d: %v", rec.Email, ibId, err))
|
||||
continue
|
||||
}
|
||||
clientsToAdd = append(clientsToAdd, client)
|
||||
}
|
||||
|
||||
if len(clientsToAdd) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(map[string][]model.Client{"clients": clientsToAdd})
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
|
||||
continue
|
||||
}
|
||||
nr, err := s.AddInboundClient(inboundSvc, &model.Inbound{Id: ibId, Settings: string(payload)})
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("inbound %d: %v", ibId, err))
|
||||
continue
|
||||
}
|
||||
if nr {
|
||||
needRestart = true
|
||||
}
|
||||
for _, c := range clientsToAdd {
|
||||
result.Attached = append(result.Attached, c.Email)
|
||||
}
|
||||
}
|
||||
|
||||
return result, needRestart, nil
|
||||
}
|
||||
|
||||
func (s *ClientService) DetachByEmailMany(inboundSvc *InboundService, email string, inboundIds []int) (bool, error) {
|
||||
if email == "" {
|
||||
return false, common.NewError("client email is required")
|
||||
|
||||
@@ -298,6 +298,14 @@
|
||||
"delAllClients": "Delete All Clients",
|
||||
"delAllClientsConfirmTitle": "Delete all {count} clients from \"{remark}\"?",
|
||||
"delAllClientsConfirmContent": "This removes every client from this inbound and drops their traffic records. The inbound itself is kept. This cannot be undone.",
|
||||
"attachClients": "Attach Clients To…",
|
||||
"assignClientsGroup": "Assign Clients To Group…",
|
||||
"attachClientsTitle": "Attach clients from \"{remark}\"",
|
||||
"attachClientsDesc": "Attaches the same {count} clients (same UUID/password and shared traffic) to the selected inbound(s). They stay on this inbound too.",
|
||||
"attachClientsTargets": "Target inbounds",
|
||||
"attachClientsNoTargets": "No other compatible inbounds available to attach to.",
|
||||
"attachClientsResult": "Attached {attached}, skipped {skipped}.",
|
||||
"attachClientsResultMixed": "Attached {attached}, skipped {skipped}, errors {errors}.",
|
||||
"exportLinksTitle": "Export inbound links",
|
||||
"exportSubsTitle": "Export subscription links",
|
||||
"exportAllLinksTitle": "Export all inbound links",
|
||||
|
||||
@@ -293,6 +293,14 @@
|
||||
"delAllClients": "حذف همه کلاینتها",
|
||||
"delAllClientsConfirmTitle": "حذف هر {count} کلاینت اینباند «{remark}»؟",
|
||||
"delAllClientsConfirmContent": "تمام کلاینتهای این اینباند به همراه رکوردهای ترافیکشان حذف میشوند. خود اینباند باقی میماند. این عمل غیرقابل بازگشت است.",
|
||||
"attachClients": "اتصال کلاینتها به…",
|
||||
"assignClientsGroup": "افزودن کلاینتها به گروه…",
|
||||
"attachClientsTitle": "اتصال کلاینتهای «{remark}»",
|
||||
"attachClientsDesc": "همان {count} کلاینت (با همان UUID/پسورد و ترافیک مشترک) را به اینباند(های) انتخابشده هم متصل میکند. روی این اینباند هم باقی میمانند.",
|
||||
"attachClientsTargets": "اینباندهای مقصد",
|
||||
"attachClientsNoTargets": "اینباند سازگار دیگری برای اتصال وجود ندارد.",
|
||||
"attachClientsResult": "{attached} متصل شد، {skipped} رد شد.",
|
||||
"attachClientsResultMixed": "{attached} متصل شد، {skipped} رد شد، {errors} خطا.",
|
||||
"exportLinksTitle": "خروجی لینکهای اینباند",
|
||||
"exportSubsTitle": "خروجی لینکهای ساب",
|
||||
"exportAllLinksTitle": "خروجی لینکهای همه اینباندها",
|
||||
|
||||
Reference in New Issue
Block a user