Skip to content

Commit

Permalink
Group Management of Subscription Clients (#2644)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
alirahimi818 authored Jan 21, 2025
1 parent 66fe841 commit 6e9180a
Showing 26 changed files with 818 additions and 71 deletions.
1 change: 1 addition & 0 deletions web/assets/js/model/setting.js
Original file line number Diff line number Diff line change
@@ -26,6 +26,7 @@ class AllSetting {
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.subEnable = false;
this.subSyncEnable = true;
this.subListen = "";
this.subPort = 2096;
this.subPath = "/sub/";
35 changes: 35 additions & 0 deletions web/assets/js/util/utils.js
Original file line number Diff line number Diff line change
@@ -70,6 +70,41 @@ class HttpUtil {
}
return msg;
}

static async jsonPost(url, data) {
let msg;
try {
const requestOptions = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
};
const resp = await fetch(url, requestOptions);
const response = await resp.json();

msg = this._respToMsg({data : response});
} catch (e) {
msg = new Msg(false, e.toString());
}
this._handleMsg(msg);
return msg;
}

static async postWithModalJson(url, data, modal) {
if (modal) {
modal.loading(true);
}
const msg = await this.jsonPost(url, data);
if (modal) {
modal.loading(false);
if (msg instanceof Msg && msg.success) {
modal.close();
}
}
return msg;
}
}

class PromiseUtil {
154 changes: 153 additions & 1 deletion web/controller/inbound.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
package controller

import (
"errors"
"encoding/json"
"fmt"
"strconv"

"x-ui/database/model"
"x-ui/web/service"
"x-ui/web/session"
@@ -33,9 +33,13 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient)
g.POST("/addGroupClient", a.addGroupInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/delGroupClients", a.delGroupClients)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/updateClients", a.updateGroupInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetGroupClientTraffic", a.resetGroupClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
@@ -190,6 +194,34 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
}
}

func (a *InboundController) addGroupInboundClient(c *gin.Context) {
var requestData []model.Inbound

err := c.ShouldBindJSON(&requestData)

if err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}

needRestart := true

for _, data := range requestData {

needRestart, err = a.inboundService.AddInboundClient(&data)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
}

jsonMsg(c, "Client(s) added", nil)
if err == nil && needRestart {
a.xrayService.SetToNeedRestart()
}

}

func (a *InboundController) delInboundClient(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
@@ -211,6 +243,38 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
}
}

func (a *InboundController) delGroupClients(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"`
ClientID string `json:"clientId"`
}

if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}

needRestart := false

for _, req := range requestData {
needRestartTmp, err := a.inboundService.DelInboundClient(req.InboundID, req.ClientID)
if err != nil {
jsonMsg(c, "Failed to delete client", err)
return
}

if needRestartTmp {
needRestart = true
}
}

jsonMsg(c, "Clients deleted successfully", nil)

if needRestart {
a.xrayService.SetToNeedRestart()
}
}

func (a *InboundController) updateInboundClient(c *gin.Context) {
clientId := c.Param("clientId")

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

func (a *InboundController) updateGroupInboundClient(c *gin.Context) {
var requestData []map[string]interface{}

if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, I18nWeb(c, "pages.inbounds.update"), err)
return
}

needRestart := false

for _, item := range requestData {

inboundMap, ok := item["inbound"].(map[string]interface{})
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'inbound' to map"))
return
}

clientId, ok := item["clientId"].(string)
if !ok {
jsonMsg(c, "Something went wrong!", errors.New("Failed to convert 'clientId' to string"))
return
}

inboundJSON, err := json.Marshal(inboundMap)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}

var inboundModel model.Inbound
if err := json.Unmarshal(inboundJSON, &inboundModel); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}

if restart, err := a.inboundService.UpdateInboundClient(&inboundModel, clientId); err != nil {
jsonMsg(c, "Something went wrong!", err)
return
} else {
needRestart = needRestart || restart
}
}

jsonMsg(c, "Client updated", nil)
if needRestart {
a.xrayService.SetToNeedRestart()
}
}

func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
@@ -253,6 +367,44 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
}
}

func (a *InboundController) resetGroupClientTraffic(c *gin.Context) {
var requestData []struct {
InboundID int `json:"inboundId"` // Map JSON "inboundId" to struct field "InboundID"
Email string `json:"email"` // Map JSON "email" to struct field "Email"
}

// Parse JSON body directly using ShouldBindJSON
if err := c.ShouldBindJSON(&requestData); err != nil {
jsonMsg(c, "Invalid request data", err)
return
}

needRestart := false

// Process each request data
for _, req := range requestData {
needRestartTmp, err := a.inboundService.ResetClientTraffic(req.InboundID, req.Email)
if err != nil {
jsonMsg(c, "Failed to reset client traffic", err)
return
}

// If any request requires a restart, set needRestart to true
if needRestartTmp {
needRestart = true
}
}

// Send response back to the client
jsonMsg(c, "Traffic reset for all clients", nil)

// Restart the service if required
if needRestart {
a.xrayService.SetToNeedRestart()
}
}


func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {
1 change: 1 addition & 0 deletions web/entity/entity.go
Original file line number Diff line number Diff line change
@@ -40,6 +40,7 @@ type AllSetting struct {
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubSyncEnable bool `json:"subSyncEnable" form:"subSyncEnable"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`
SubPath string `json:"subPath" form:"subPath"`
24 changes: 15 additions & 9 deletions web/html/common/qrcode_modal.html
Original file line number Diff line number Diff line change
@@ -23,13 +23,15 @@
</tr-qr-bg>
</tr-qr-box>
</template>
<template v-for="(row, index) in qrModal.qrcodes">
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
<template v-if="!isJustSub">
<template v-for="(row, index) in qrModal.qrcodes">
<tr-qr-box class="qr-box">
<a-tag color="green" class="qr-tag"><span>[[ row.remark ]]</span></a-tag>
<tr-qr-bg class="qr-bg">
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" class="qr-cv"></canvas>
</tr-qr-bg>
</tr-qr-box>
</template>
</template>
</tr-qr-modal>
</a-modal>
@@ -43,12 +45,14 @@
qrcodes: [],
clipboard: null,
visible: false,
isJustSub: false,
subId: '',
show: function(title = '', dbInbound, client) {
show: function(title = '', dbInbound, client, isJustSub = false) {
this.title = title;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
this.client = client;
this.isJustSub = isJustSub;
this.subId = '';
this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD) {
@@ -76,7 +80,9 @@
delimiters: ['[[', ']]'],
el: '#qrcode-modal',
data: {
qrModal: qrModal,
qrModal: qrModal,get isJustSub(){
return qrModal.isJustSub
}
},
methods: {
copyToClipboard(elementId, content) {
Loading

1 comment on commit 6e9180a

@OramenLive
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work bro, but how can a panel admin use this feature?

Please sign in to comment.