diff options
Diffstat (limited to 'web')
26 files changed, 818 insertions, 71 deletions
diff --git a/web/assets/js/model/setting.js b/web/assets/js/model/setting.js index 8e010598..72fe77ef 100644 --- a/web/assets/js/model/setting.js +++ b/web/assets/js/model/setting.js @@ -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/"; diff --git a/web/assets/js/util/utils.js b/web/assets/js/util/utils.js index 30f1f6a2..28ec95c7 100644 --- a/web/assets/js/util/utils.js +++ b/web/assets/js/util/utils.js @@ -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 { diff --git a/web/controller/inbound.go b/web/controller/inbound.go index c22ce192..a8003484 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -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 { diff --git a/web/entity/entity.go b/web/entity/entity.go index 12206340..f5f375ea 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -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"` diff --git a/web/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html index 94e750c7..6558c347 100644 --- a/web/html/common/qrcode_modal.html +++ b/web/html/common/qrcode_modal.html @@ -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) { diff --git a/web/html/xui/client_modal.html b/web/html/xui/client_modal.html index aa62e02a..de7915a5 100644 --- a/web/html/xui/client_modal.html +++ b/web/html/xui/client_modal.html @@ -16,7 +16,16 @@ title: '', okText: '', isEdit: false, + group: { + canGroup: true, + isGroup: false, + currentClient: null, + inbounds: [], + clients: [], + editIds: [] + }, dbInbound: new DBInbound(), + dbInbounds: null, inbound: new Inbound(), clients: [], clientStats: [], @@ -25,33 +34,121 @@ clientIps: null, delayedStart: false, ok() { - if (clientModal.isEdit) { - ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId); + if (app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup) { + const currentClient = clientModal.group.currentClient; + const { limitIp, totalGB, expiryTime, reset, enable, subId, tgId, flow } = currentClient; + const uniqueEmails = clientModalApp.makeGroupEmailsUnique(clientModal.dbInbounds, currentClient.email, clientModal.group.clients); + + clientModal.group.clients.forEach((client, index) => { + client.email = uniqueEmails[index]; + client.limitIp = limitIp; + client.totalGB = totalGB; + client.expiryTime = expiryTime; + client.reset = reset; + client.enable = enable; + + if (subId) { + client.subId = subId; + } + if (tgId) { + client.tgId = tgId; + } + if (flow) { + client.flow = flow; + } + }); + + if (clientModal.isEdit) { + ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds, clientModal.group.editIds); + } else { + ObjectUtil.execute(clientModal.confirm, clientModal.group.clients, clientModal.group.inbounds); + } } else { - ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id); + if (clientModal.isEdit) { + ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId); + } else { + ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id); + } } }, - show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) { + show({ + title = '', + okText = '{{ i18n "sure" }}', + index = null, + dbInbound = null, + dbInbounds = null, + confirm = () => { + }, + isEdit = false + }) { + this.group = { + canGroup: true, + isGroup: false, + currentClient: null, + inbounds: [], + clients: [], + editIds: [] + } + this.dbInbounds = dbInbounds; this.visible = true; this.title = title; this.okText = okText; this.isEdit = isEdit; + if (app.subSettings.enable && dbInbounds !== null && Array.isArray(dbInbounds)) { + if (isEdit) { + this.showProcess(dbInbound, index); + let processSingleEdit = true + if (this.group.canGroup) { + this.group.currentClient = this.clients[this.index] + const response = app.getSubGroupClients(dbInbounds, this.group.currentClient) + if (response.clients.length > 1) { + this.group.isGroup = true; + this.group.inbounds = response.inbounds + this.group.clients = response.clients + this.group.editIds = response.editIds + if (this.clients[index].expiryTime < 0) { + this.delayedStart = true; + } + processSingleEdit = false + } + } + if (processSingleEdit) { + this.singleEditClientProcess(index) + } + } else { + this.group.isGroup = true; + dbInbounds.forEach((dbInboundItem) => { + this.showProcess(dbInboundItem); + this.addClient(this.inbound.protocol, this.clients); + this.group.inbounds.push(dbInboundItem.id) + this.group.clients.push(this.clients[this.index]) + }) + this.group.currentClient = this.clients[this.index] + } + } else { + this.showProcess(dbInbound, index); + if (isEdit) { + this.singleEditClientProcess(index) + } else { + this.addClient(this.inbound.protocol, this.clients); + } + } + this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email); + this.confirm = confirm; + }, + showProcess(dbInbound, index = null) { this.dbInbound = new DBInbound(dbInbound); this.inbound = dbInbound.toInbound(); this.clients = this.inbound.clients; this.index = index === null ? this.clients.length : index; this.delayedStart = false; - if (isEdit) { - if (this.clients[index].expiryTime < 0) { - this.delayedStart = true; - } - this.oldClientId = this.getClientId(dbInbound.protocol, clients[index]); - } else { - this.addClient(this.inbound.protocol, this.clients); + }, + singleEditClientProcess(index) { + if (this.clients[index].expiryTime < 0) { + this.delayedStart = true; } - this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email); - this.confirm = confirm; - }, + this.oldClientId = this.getClientId(this.dbInbound.protocol, this.clients[index]); + }, getClientId(protocol, client) { switch (protocol) { case Protocols.TROJAN: return client.password; @@ -72,7 +169,7 @@ clientModal.visible = false; clientModal.loading(false); }, - loading(loading=true) { + loading(loading = true) { clientModal.confirmLoading = loading; }, }; @@ -94,6 +191,18 @@ get isEdit() { return this.clientModal.isEdit; }, + get isGroup() { + return this.clientModal.group.isGroup; + }, + get isGroupEdit() { + return this.clientModal.group.canGroup; + }, + set isGroupEdit(value) { + this.clientModal.group.canGroup = value; + if (!value) { + this.clientModal.singleEditClientProcess(this.clientModal.index) + } + }, get datepicker() { return app.datepicker; }, @@ -120,22 +229,35 @@ }, }, methods: { - async getDBClientIps(email) { - const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`); - if (!msg.success) { - document.getElementById("clientIPs").value = msg.obj; - return; - } - let ips = msg.obj; - if (typeof ips === 'string' && ips.startsWith('[') && ips.endsWith(']')) { - try { - ips = JSON.parse(ips); - ips = Array.isArray(ips) ? ips.join("\n") : ips; - } catch (e) { - console.error('Error parsing JSON:', e); - } + makeGroupEmailsUnique(dbInbounds, baseEmail, groupClients) { + // Extract the base part of the email (before the "__" if present) + const match = baseEmail.match(/^(.*?)__/); + const base = match ? match[1] : baseEmail; + + // Generate initial emails for each client in the group + const generatedEmails = groupClients.map((_, index) => `${base}__${index + 1}`); + + // Function to check if an email already exists in dbInbounds but belongs to a different subId + const isDuplicate = (emailToCheck, clientSubId) => { + return dbInbounds.some((dbInbound) => { + const settings = JSON.parse(dbInbound.settings); + const clients = settings && settings.clients ? settings.clients : []; + return clients.some(client => client.email === emailToCheck && client.subId !== clientSubId); + }); + }; + + // Check if any of the generated emails are duplicates + const hasDuplicates = generatedEmails.some((email, index) => { + return isDuplicate(email, groupClients[index].subId); + }); + + // If duplicates exist, add a random string to the base email to ensure uniqueness + if (hasDuplicates) { + const randomString = `-${RandomUtil.randomLowerAndNum(4)}`; + return groupClients.map((_, index) => `${base}${randomString}__${index + 1}`); } - document.getElementById("clientIPs").value = ips; + + return generatedEmails; }, async clearDBClientIps(email) { try { @@ -147,7 +269,22 @@ } catch (error) { } }, - resetClientTraffic(email, dbInboundId, iconElement) { + async resetClientTrafficHandler(client, dbInboundId, clients = []) { + if (clients.length > 0) { + const resetRequests = clients + .filter(client => { + const inbound = clientModal.dbInbounds.find(inbound => inbound.id === client.inboundId); + return inbound && app.hasClientStats(inbound, client.email); + }).map(client => ({ inboundId: client.inboundId, email: client.email})); + + return HttpUtil.postWithModalJson('/panel/inbound/resetGroupClientTraffic', resetRequests, null) + } else { + return HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email) + } + }, + resetClientTraffic(client, dbInboundId, iconElement) { + const subGroup = app.subSettings.enable && clientModal.group.isGroup && clientModal.group.canGroup && clientModal.dbInbounds && clientModal.dbInbounds.length > 0 ? app.getSubGroupClients(clientModal.dbInbounds, client) : []; + const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : []; this.$confirm({ title: '{{ i18n "pages.inbounds.resetTraffic"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', @@ -156,8 +293,8 @@ cancelText: '{{ i18n "cancel"}}', onOk: async () => { iconElement.disabled = true; - const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email); - if (msg.success) { + const msg = await this.resetClientTrafficHandler(client, dbInboundId, clients); + if (msg && msg.success) { this.clientModal.clientStats.up = 0; this.clientModal.clientStats.down = 0; } diff --git a/web/html/xui/form/client.html b/web/html/xui/form/client.html index 0b894f01..a0a1ced8 100644 --- a/web/html/xui/form/client.html +++ b/web/html/xui/form/client.html @@ -3,6 +3,18 @@ <a-form-item label='{{ i18n "pages.inbounds.enable" }}'> <a-switch v-model="client.enable"></a-switch> </a-form-item> + <a-form-item v-if="isEdit && app.subSettings.enable && isGroup"> + <template slot="label"> + <a-tooltip> + <template slot="title"> + <span>{{ i18n "pages.client.isGroupEditDesc" }}</span> + </template> + {{ i18n "pages.client.isGroupEdit" }} + <a-icon type="question-circle"></a-icon> + </a-tooltip> + </template> + <a-switch v-model="isGroupEdit"></a-switch> + </a-form-item> <a-form-item> <template slot="label"> <a-tooltip> @@ -134,7 +146,7 @@ <a-tooltip> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <a-icon type="retweet" - @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" + @click="resetClientTraffic(client,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon> </a-tooltip> </a-form-item> diff --git a/web/html/xui/inbound_client_table.html b/web/html/xui/inbound_client_table.html index 13593cea..74d77eab 100644 --- a/web/html/xui/inbound_client_table.html +++ b/web/html/xui/inbound_client_table.html @@ -12,7 +12,7 @@ <template slot="title">{{ i18n "info" }}</template> <a-icon style="font-size: 24px;" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon> </a-tooltip> - <a-tooltip> + <a-tooltip v-if="hasClientStats(record, client.email)"> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'> <a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon> diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html index 4de3518c..66e4b7b7 100644 --- a/web/html/xui/inbound_modal.html +++ b/web/html/xui/inbound_modal.html @@ -14,6 +14,7 @@ confirmLoading: false, okText: '{{ i18n "sure" }}', isEdit: false, + isGroup: false, confirm: null, inbound: new Inbound(), dbInbound: new DBInbound(), @@ -61,6 +62,9 @@ get isEdit() { return inModal.isEdit; }, + get isGroup() { + return inModal.isGroup; + }, get client() { return inModal.inbound.clients[0]; }, diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 478f29de..5d4d918b 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -224,6 +224,10 @@ <a-icon type="rest"></a-icon> {{ i18n "pages.inbounds.delDepletedClients" }} </a-menu-item> + <a-menu-item v-if="subSettings.enable && dbInbounds.length > 0" key="addGroupClient"> + <a-icon type="usergroup-add"></a-icon> + {{ i18n "pages.client.groupAdd"}} + </a-menu-item> </a-menu> </a-dropdown> </a-col> @@ -859,6 +863,9 @@ case "delDepletedClients": this.delDepletedClients(-1) break; + case "addGroupClient": + this.openGroupAddClient() + break; } }, clickAction(action, dbInbound) { @@ -1004,6 +1011,21 @@ await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal); }, + openGroupAddClient() { + clientModal.show({ + title: '{{ i18n "pages.client.groupAdd"}}', + okText: '{{ i18n "pages.client.submitAdd"}}', + dbInbounds: this.dbInbounds, + confirm: async (clients, dbInboundIds) => { + await this.addGroupClient(clients, dbInboundIds, clientModal).then((res) => { + if(res){ + this.showQrcode(dbInboundIds[0],clients[0], true) + } + }); + }, + isEdit: false + }); + }, openAddClient(dbInboundId) { dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); clientModal.show({ @@ -1011,7 +1033,11 @@ okText: '{{ i18n "pages.client.submitAdd"}}', dbInbound: dbInbound, confirm: async (clients, dbInboundId) => { - await this.addClient(clients, dbInboundId, clientModal); + await this.addClient(clients, dbInboundId, clientModal).then((res) => { + if(res){ + this.showQrcode(dbInboundId,clients) + } + }); }, isEdit: false }); @@ -1034,6 +1060,7 @@ clientModal.show({ title: '{{ i18n "pages.client.edit"}}', okText: '{{ i18n "pages.client.submitEdit"}}', + dbInbounds: this.dbInbounds, dbInbound: dbInbound, index: index, confirm: async (client, dbInboundId, clientId) => { @@ -1059,12 +1086,36 @@ }; await this.submit(`/panel/inbound/addClient`, data, modal); }, + async addGroupClient(clients, dbInboundIds, modal) { + const data = [] + dbInboundIds.forEach((dbInboundId, index) => { + data.push({ + id: dbInboundId, + settings: '{"clients": [' + clients[index].toString() + ']}', + }) + }) + retur
|
