Skip to content

Commit 6e9180a

Browse files
authored
Group Management of Subscription Clients (#2644)
* add group user with the same subscription id to all inbounds * code format compare * add await for reset client traffic * en language changed * added client traffic syncer job * handle exist email duplicate in sub group * multi reset and delete request for clients group * add client traffic syncer setting option * vi translate file updated * auto open qr-modal bug fixed
1 parent 66fe841 commit 6e9180a

26 files changed

+818
-71
lines changed

web/assets/js/model/setting.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ class AllSetting {
2626
this.xrayTemplateConfig = "";
2727
this.secretEnable = false;
2828
this.subEnable = false;
29+
this.subSyncEnable = true;
2930
this.subListen = "";
3031
this.subPort = 2096;
3132
this.subPath = "/sub/";

web/assets/js/util/utils.js

+35
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,41 @@ class HttpUtil {
7070
}
7171
return msg;
7272
}
73+
74+
static async jsonPost(url, data) {
75+
let msg;
76+
try {
77+
const requestOptions = {
78+
method: 'POST',
79+
headers: {
80+
'Content-Type': 'application/json',
81+
},
82+
body: JSON.stringify(data),
83+
};
84+
const resp = await fetch(url, requestOptions);
85+
const response = await resp.json();
86+
87+
msg = this._respToMsg({data : response});
88+
} catch (e) {
89+
msg = new Msg(false, e.toString());
90+
}
91+
this._handleMsg(msg);
92+
return msg;
93+
}
94+
95+
static async postWithModalJson(url, data, modal) {
96+
if (modal) {
97+
modal.loading(true);
98+
}
99+
const msg = await this.jsonPost(url, data);
100+
if (modal) {
101+
modal.loading(false);
102+
if (msg instanceof Msg && msg.success) {
103+
modal.close();
104+
}
105+
}
106+
return msg;
107+
}
73108
}
74109

75110
class PromiseUtil {

web/controller/inbound.go

+153-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package controller
22

33
import (
4+
"errors"
45
"encoding/json"
56
"fmt"
67
"strconv"
7-
88
"x-ui/database/model"
99
"x-ui/web/service"
1010
"x-ui/web/session"
@@ -33,9 +33,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
3333
g.POST("/clientIps/:email", a.getClientIps)
3434
g.POST("/clearClientIps/:email", a.clearClientIps)
3535
g.POST("/addClient", a.addInboundClient)
36+
g.POST("/addGroupClient", a.addGroupInboundClient)
3637
g.POST("/:id/delClient/:clientId", a.delInboundClient)
38+
g.POST("/delGroupClients", a.delGroupClients)
3739
g.POST("/updateClient/:clientId", a.updateInboundClient)
40+
g.POST("/updateClients", a.updateGroupInboundClient)
3841
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
42+
g.POST("/resetGroupClientTraffic", a.resetGroupClientTraffic)
3943
g.POST("/resetAllTraffics", a.resetAllTraffics)
4044
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
4145
g.POST("/delDepletedClients/:id", a.delDepletedClients)
@@ -190,6 +194,34 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
190194
}
191195
}
192196

197+
func (a *InboundController) addGroupInboundClient(c *gin.Context) {
198+
var requestData []model.Inbound
199+
200+
err := c.ShouldBindJSON(&requestData)
201+
202+
if err != nil {
203+
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
204+
return
205+
}
206+
207+
needRestart := true
208+
209+
for _, data := range requestData {
210+
211+
needRestart, err = a.inboundService.AddInboundClient(&data)
212+
if err != nil {
213+
jsonMsg(c, "Something went wrong!", err)
214+
return
215+
}
216+
}
217+
218+
jsonMsg(c, "Client(s) added", nil)
219+
if err == nil && needRestart {
220+
a.xrayService.SetToNeedRestart()
221+
}
222+
223+
}
224+
193225
func (a *InboundController) delInboundClient(c *gin.Context) {
194226
id, err := strconv.Atoi(c.Param("id"))
195227
if err != nil {
@@ -211,6 +243,38 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
211243
}
212244
}
213245

246+
func (a *InboundController) delGroupClients(c *gin.Context) {
247+
var requestData []struct {
248+
InboundID int `json:"inboundId"`
249+
ClientID string `json:"clientId"`
250+
}
251+
252+
if err := c.ShouldBindJSON(&requestData); err != nil {
253+
jsonMsg(c, "Invalid request data", err)
254+
return
255+
}
256+
257+
needRestart := false
258+
259+
for _, req := range requestData {
260+
needRestartTmp, err := a.inboundService.DelInboundClient(req.InboundID, req.ClientID)
261+
if err != nil {
262+
jsonMsg(c, "Failed to delete client", err)
263+
return
264+
}
265+
266+
if needRestartTmp {
267+
needRestart = true
268+
}
269+
}
270+
271+
jsonMsg(c, "Clients deleted successfully", nil)
272+
273+
if needRestart {
274+
a.xrayService.SetToNeedRestart()
275+
}
276+
}
277+
214278
func (a *InboundController) updateInboundClient(c *gin.Context) {
215279
clientId := c.Param("clientId")
216280

@@ -234,6 +298,56 @@ func (a *InboundController) updateInboundClient(c *gin.Context) {
234298
}
235299
}
236300

301+
func (a *InboundController) updateGroupInboundClient(c *gin.Context) {
302+
var requestData []map[string]interface{}
303+
304+
if err := c.ShouldBindJSON(&requestData); err != nil {
305+
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
306+
return
307+
}
308+
309+
needRestart := false
310+
311+
for _, item := range requestData {
312+
313+
inboundMap, ok := item["inbound"].(map[string]interface{})
314+
if !ok {
315+
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'inbound' to map"))
316+
return
317+
}
318+
319+
clientId, ok := item["clientId"].(string)
320+
if !ok {
321+
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'clientId' to string"))
322+
return
323+
}
324+
325+
inboundJSON, err := json.Marshal(inboundMap)
326+
if err != nil {
327+
jsonMsg(c, "Something went wrong!", err)
328+
return
329+
}
330+
331+
var inboundModel model.Inbound
332+
if err := json.Unmarshal(inboundJSON, &inboundModel); err != nil {
333+
jsonMsg(c, "Something went wrong!", err)
334+
return
335+
}
336+
337+
if restart, err := a.inboundService.UpdateInboundClient(&inboundModel, clientId); err != nil {
338+
jsonMsg(c, "Something went wrong!", err)
339+
return
340+
} else {
341+
needRestart = needRestart || restart
342+
}
343+
}
344+
345+
jsonMsg(c, "Client updated", nil)
346+
if needRestart {
347+
a.xrayService.SetToNeedRestart()
348+
}
349+
}
350+
237351
func (a *InboundController) resetClientTraffic(c *gin.Context) {
238352
id, err := strconv.Atoi(c.Param("id"))
239353
if err != nil {
@@ -253,6 +367,44 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
253367
}
254368
}
255369

370+
func (a *InboundController) resetGroupClientTraffic(c *gin.Context) {
371+
var requestData []struct {
372+
InboundID int `json:"inboundId"` // Map JSON "inboundId" to struct field "InboundID"
373+
Email string `json:"email"` // Map JSON "email" to struct field "Email"
374+
}
375+
376+
// Parse JSON body directly using ShouldBindJSON
377+
if err := c.ShouldBindJSON(&requestData); err != nil {
378+
jsonMsg(c, "Invalid request data", err)
379+
return
380+
}
381+
382+
needRestart := false
383+
384+
// Process each request data
385+
for _, req := range requestData {
386+
needRestartTmp, err := a.inboundService.ResetClientTraffic(req.InboundID, req.Email)
387+
if err != nil {
388+
jsonMsg(c, "Failed to reset client traffic", err)
389+
return
390+
}
391+
392+
// If any request requires a restart, set needRestart to true
393+
if needRestartTmp {
394+
needRestart = true
395+
}
396+
}
397+
398+
// Send response back to the client
399+
jsonMsg(c, "Traffic reset for all clients", nil)
400+
401+
// Restart the service if required
402+
if needRestart {
403+
a.xrayService.SetToNeedRestart()
404+
}
405+
}
406+
407+
256408
func (a *InboundController) resetAllTraffics(c *gin.Context) {
257409
err := a.inboundService.ResetAllTraffics()
258410
if err != nil {

web/entity/entity.go

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ type AllSetting struct {
4040
TimeLocation string `json:"timeLocation" form:"timeLocation"`
4141
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
4242
SubEnable bool `json:"subEnable" form:"subEnable"`
43+
SubSyncEnable bool `json:"subSyncEnable" form:"subSyncEnable"`
4344
SubListen string `json:"subListen" form:"subListen"`
4445
SubPort int `json:"subPort" form:"subPort"`
4546
SubPath string `json:"subPath" form:"subPath"`

web/html/common/qrcode_modal.html

+15-9
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
</tr-qr-bg>
2424
</tr-qr-box>
2525
</template>
26-
<template v-for="(row, index) in qrModal.qrcodes">
27-
<tr-qr-box class="qr-box">
28-
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
29-
<tr-qr-bg class="qr-bg">
30-
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
31-
</tr-qr-bg>
32-
</tr-qr-box>
26+
<template v-if="!isJustSub">
27+
<template v-for="(row, index) in qrModal.qrcodes">
28+
<tr-qr-box class="qr-box">
29+
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
30+
<tr-qr-bg class="qr-bg">
31+
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
32+
</tr-qr-bg>
33+
</tr-qr-box>
34+
</template>
3335
</template>
3436
</tr-qr-modal>
3537
</a-modal>
@@ -43,12 +45,14 @@
4345
qrcodes: [],
4446
clipboard: null,
4547
visible: false,
48+
isJustSub: false,
4649
subId: '',
47-
show: function(title = '', dbInbound, client) {
50+
show: function(title = '', dbInbound, client, isJustSub = false) {
4851
this.title = title;
4952
this.dbInbound = dbInbound;
5053
this.inbound = dbInbound.toInbound();
5154
this.client = client;
55+
this.isJustSub = isJustSub;
5256
this.subId = '';
5357
this.qrcodes = [];
5458
if (this.inbound.protocol == Protocols.WIREGUARD) {
@@ -76,7 +80,9 @@
7680
delimiters: ['[[', ']]'],
7781
el: '#qrcode-modal',
7882
data: {
79-
qrModal: qrModal,
83+
qrModal: qrModal,get isJustSub(){
84+
return qrModal.isJustSub
85+
}
8086
},
8187
methods: {
8288
copyToClipboard(elementId, content) {

0 commit comments

Comments
 (0)