-
+
Config
@@ -418,6 +444,18 @@
diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html
index f605b4fd..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];
},
@@ -102,11 +106,6 @@
client.flow = "";
});
}
- if ((this.inModal.inbound.protocol == Protocols.VLESS || this.inModal.inbound.protocol == Protocols.TROJAN) && !inModal.inbound.xtls) {
- this.inModal.inbound.settings.vlesses.forEach(client => {
- client.flow = "";
- });
- }
},
SSMethodChange() {
if (this.inModal.inbound.isSSMultiUser) {
@@ -132,10 +131,6 @@
inModal.inbound.stream.tls.certs[index].certFile = app.defaultCert;
inModal.inbound.stream.tls.certs[index].keyFile = app.defaultKey;
},
- setDefaultCertXtls(index) {
- inModal.inbound.stream.xtls.certs[index].certFile = app.defaultCert;
- inModal.inbound.stream.xtls.certs[index].keyFile = app.defaultKey;
- },
async getNewX25519Cert() {
inModal.loading(true);
const msg = await HttpUtil.post('/server/getNewX25519Cert');
diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html
index 1aad5d91..1cd1be1a 100644
--- a/web/html/xui/inbounds.html
+++ b/web/html/xui/inbounds.html
@@ -15,7 +15,7 @@
overflow-y: hidden;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
- margin:-10px 22px -10px !important;
+ margin:-10px 22px !important;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
border-bottom-left-radius: 1rem;
@@ -40,7 +40,7 @@
padding: .5rem;
}
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
- margin:-10px 2px -10px !important;
+ margin:-10px 2px !important;
}
}
.ant-col-sm-24 {
@@ -224,6 +224,10 @@
{{ i18n "pages.inbounds.delDepletedClients" }}
+
+
+ {{ i18n "pages.client.groupAdd"}}
+
@@ -338,7 +342,6 @@
[[ dbInbound.toInbound().stream.network ]]
TLS
- XTLS
Reality
@@ -549,7 +552,7 @@
-
+
{{template "component/themeSwitcher" .}}
{{template "component/persianDatepicker" .}}
@@ -860,6 +863,9 @@
case "delDepletedClients":
this.delDepletedClients(-1)
break;
+ case "addGroupClient":
+ this.openGroupAddClient()
+ break;
}
},
clickAction(action, dbInbound) {
@@ -1005,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({
@@ -1012,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
});
@@ -1035,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) => {
@@ -1060,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() + ']}',
+ })
+ })
+ return await this.submit(`/panel/inbound/addGroupClient`, data, modal, true)
+ },
async updateClient(client, dbInboundId, clientId) {
- const data = {
- id: dbInboundId,
- settings: '{"clients": [' + client.toString() + ']}',
- };
- await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
+ if (Array.isArray(client) && Array.isArray(dbInboundId) && Array.isArray(clientId)){
+ const data = []
+ client.forEach((client, index) => {
+ data.push({
+ clientId: clientId[index],
+ inbound: {
+ id: dbInboundId[index],
+ settings: '{"clients": [' + client.toString() + ']}',
+ }
+ })
+ })
+ await this.submit(`/panel/inbound/updateClients`, data, clientModal, true);
+ }else{
+ const data = {
+ id: dbInboundId,
+ settings: '{"clients": [' + client.toString() + ']}',
+ };
+ await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
+ }
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1093,22 +1143,100 @@
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
});
},
- delClient(dbInboundId, client,confirmation = true) {
- dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
- clientId = this.getClientId(dbInbound.protocol, client);
+ async delClientHandler(dbInboundId, currentClient, clients = []) {
+ if (clients.length > 0) {
+ const deleteRequestData = [];
+
+ for (const client of clients) {
+ const dbInbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
+ if (dbInbound) {
+ const inbound = dbInbound.toInbound();
+ if (inbound && inbound.clients && inbound.clients.length === 1) {
+ let newClient = Inbound.Settings.getSettings(inbound.protocol).toString();
+ newClient = JSON.parse(newClient);
+ newClient = newClient && newClient.clients && newClient.clients.length > 0 ? JSON.stringify(newClient.clients[0], null, 2) : null;
+ if (newClient) {
+ const data = {
+ id: client.inboundId,
+ settings: '{"clients": [' + newClient + ']}',
+ };
+ await this.submit(`/panel/inbound/addClient`, data, null);
+ }
+ }
+
+ deleteRequestData.push({
+ inboundId: client.inboundId,
+ clientId: client.clientId,
+ });
+ }
+ }
+ await this.submit('/panel/inbound/delGroupClients', deleteRequestData, null, true);
+ } else {
+ const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ const clientId = this.getClientId(dbInbound.protocol, currentClient);
+ await this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
+ }
+ },
+ delClient(dbInboundId, currentClient, confirmation = true) {
+ const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, currentClient) : [];
+ const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
if (confirmation){
+ const clientEmails = clients.length > 0 ? clients.map(item => item.email) : currentClient.email
this.$confirm({
- title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
+ title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + clientEmails,
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
- onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
+ onOk: () => this.delClientHandler(dbInboundId, currentClient, clients),
});
} else {
- this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
+ this.delClientHandler(dbInboundId, currentClient, clients)
}
},
+ getSubGroupClients(dbInbounds, currentClient) {
+ const response = {
+ inbounds: [],
+ clients: [],
+ editIds: [],
+ };
+
+ if (!Array.isArray(dbInbounds) || dbInbounds.length === 0) {
+ return response;
+ }
+ if (!currentClient || !currentClient.subId) {
+ return response;
+ }
+
+ dbInbounds.forEach((dbInboundItem) => {
+ try {
+ const dbInbound = new DBInbound(dbInboundItem);
+ if (!dbInbound) {
+ return;
+ }
+
+ const inbound = dbInbound.toInbound();
+ if (!inbound || !Array.isArray(inbound.clients)) {
+ return;
+ }
+
+ inbound.clients.forEach((client) => {
+ if (client.subId === currentClient.subId) {
+ client.inboundId = dbInboundItem.id;
+ client.clientId = this.getClientId(dbInbound.protocol, client);
+
+ response.inbounds.push(dbInboundItem.id);
+ response.clients.push(client);
+ response.editIds.push(client.clientId);
+ }
+ });
+ } catch (error) {
+ console.error("Error processing dbInboundItem:", dbInboundItem, error);
+ }
+ });
+
+ return response;
+ },
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
@@ -1136,10 +1264,10 @@
}
return newDbInbound;
},
- showQrcode(dbInboundId, client) {
+ showQrcode(dbInboundId, client, isJustSub = false) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = this.checkFallback(dbInbound);
- qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client);
+ qrModal.show('{{ i18n "qrCode"}}', newDbInbound, client, isJustSub);
},
showInfo(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -1159,36 +1287,61 @@
},
async switchEnableClient(dbInboundId, client) {
this.loading()
- dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
- inbound = dbInbound.toInbound();
- clients = inbound.clients;
- index = this.findIndexOfClient(dbInbound.protocol, clients, client);
- clients[index].enable = !clients[index].enable;
- clientId = this.getClientId(dbInbound.protocol, clients[index]);
- await this.updateClient(clients[index], dbInboundId, clientId);
+ const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
+ if (subGroup && subGroup.clients && subGroup.clients.length > 0){
+ await this.updateClient(subGroup.clients.map(item => {
+ item.enable = !item.enable
+ return item
+ }), subGroup.inbounds, subGroup.editIds);
+ }else{
+ dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
+ inbound = dbInbound.toInbound();
+ clients = inbound.clients;
+ index = this.findIndexOfClient(dbInbound.protocol, clients, client);
+ clients[index].enable = !clients[index].enable;
+ clientId = this.getClientId(dbInbound.protocol, clients[index]);
+ await this.updateClient(clients[index], dbInboundId, clientId);
+ }
this.loading(false);
},
- async submit(url, data, modal) {
- const msg = await HttpUtil.postWithModal(url, data, modal);
+ async submit(url, data, model, isJson = false) {
+ const msg = isJson ? await HttpUtil.postWithModalJson(url, data, model) : await HttpUtil.postWithModal(url, data, model);
if (msg.success) {
await this.getDBInbounds();
}
+ return msg
},
getInboundClients(dbInbound) {
return dbInbound.toInbound().clients;
},
+ resetClientTrafficHandler(client, dbInboundId, clients = []) {
+ if (clients.length > 0){
+ const resetRequests = clients
+ .filter(client => {
+ const inbound = this.dbInbounds.find(inbound => inbound.id === client.inboundId);
+ return inbound && this.hasClientStats(inbound, client.email);
+ }).map(client => ({ inboundId: client.inboundId, email: client.email}));
+
+ this.submit('/panel/inbound/resetGroupClientTraffic', resetRequests, null, true)
+ }else {
+ this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
+ }
+ },
resetClientTraffic(client, dbInboundId, confirmation = true) {
+ const subGroup = this.subSettings.enable ? this.getSubGroupClients(this.dbInbounds, client) : [];
+ const clients = subGroup && subGroup.clients && subGroup.clients.length > 1 ? subGroup.clients : [];
if (confirmation){
+ const clientEmails = clients.length > 0 ? clients.map(item => item.email) : client.email
this.$confirm({
- title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + client.email,
+ title: '{{ i18n "pages.inbounds.resetTraffic"}}' + ' ' + clientEmails,
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: themeSwitcher.currentTheme,
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
- onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
+ onOk: () => this.resetClientTrafficHandler(client, dbInboundId, clients),
})
} else {
- this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
+ this.resetClientTrafficHandler(client, dbInboundId, clients);
}
},
resetAllTraffic() {
@@ -1224,6 +1377,10 @@
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index);
},
+ hasClientStats(dbInbound, email) {
+ if (email.length == 0) return 0;
+ return !!dbInbound.clientStats.find(stats => stats.email === email);
+ },
getUpStats(dbInbound, email) {
if (email.length == 0) return 0;
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
@@ -1311,7 +1468,7 @@
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
- subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
+ subLinks.push(this.subSettings.subURI + c.subId)
}
})
}
@@ -1338,7 +1495,7 @@
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
- subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
+ subLinks.push(this.subSettings.subURI + c.subId)
}
})
}
diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html
index d7383061..ee4c7fe7 100644
--- a/web/html/xui/settings.html
+++ b/web/html/xui/settings.html
@@ -26,7 +26,7 @@
padding: .5rem 1rem;
text-align: center;
background: rgb(255 145 0 / 15%);
- margin: 1.5rem 2.5rem 0rem 2.5rem;
+ margin: 1.5rem 2.5rem 0rem;
border-radius: .5rem;
transition: all 0.5s;
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
@@ -268,6 +268,7 @@
+
@@ -381,14 +382,40 @@
-
-
-
+
+
+
+
+
+
+
+
+ [[ p.label ]]
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+ [[ p.label ]]
+
+
+
+
@@ -439,7 +466,7 @@
sockopt: {
tcpKeepAliveIdle: 100,
tcpMptcp: true,
- tcpNoDelay: true
+ penetrate: true
}
}
},
@@ -476,25 +503,26 @@
]
},
],
- IPsOptions: [
- { label: 'Private IP', value: 'private' },
- { label: '🇮🇷 Iran', value: 'ir' },
- { label: '🇨🇳 China', value: 'cn' },
- { label: '🇷🇺 Russia', value: 'ru' },
- { label: '🇻🇳 Vietnam', value: 'vn' },
- { label: '🇪🇸 Spain', value: 'es' },
- { label: '🇮🇩 Indonesia', value: 'id' },
- { label: '🇺🇦 Ukraine', value: 'ua' },
- { label: '🇹🇷 Türkiye', value: 'tr' },
- { label: '🇧🇷 Brazil', value: 'br' },
+ directIPsOptions: [
+ { label: 'Private IP', value: 'geoip:private' },
+ { label: '🇮🇷 Iran', value: 'geoip:ir' },
+ { label: '🇨🇳 China', value: 'geoip:cn' },
+ { label: '🇷🇺 Russia', value: 'geoip:ru' },
+ { label: '🇻🇳 Vietnam', value: 'geoip:vn' },
+ { label: '🇪🇸 Spain', value: 'geoip:es' },
+ { label: '🇮🇩 Indonesia', value: 'geoip:id' },
+ { label: '🇺🇦 Ukraine', value: 'geoip:ua' },
+ { label: '🇹🇷 Türkiye', value: 'geoip:tr' },
+ { label: '🇧🇷 Brazil', value: 'geoip:br' },
],
- DomainsOptions: [
- { label: '🇮🇷 Iran', value: 'ir' },
- { label: '🇨🇳 China', value: 'cn' },
- { label: '🇷🇺 Russia', value: 'ru' },
- { label: 'Apple', value: 'apple' },
- { label: 'Meta', value: 'meta' },
- { label: 'Google', value: 'google' },
+ diretDomainsOptions: [
+ { label: 'Private DNS', value: 'geosite:private' },
+ { label: '🇮🇷 Iran', value: 'geosite:category-ir' },
+ { label: '🇨🇳 China', value: 'geosite:cn' },
+ { label: '🇷🇺 Russia', value: 'geosite:category-ru' },
+ { label: 'Apple', value: 'geosite:apple' },
+ { label: 'Meta', value: 'geosite:meta' },
+ { label: 'Google', value: 'geosite:google' },
],
get remarkModel() {
rm = this.allSetting.remarkModel;
@@ -752,7 +780,7 @@
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const ipRule = rules.find(r => r.ip);
- return ipRule?.ip.map(d => d.replace("geoip:", "")) ?? [];
+ return ipRule?.ip ?? [];
},
set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
@@ -766,7 +794,7 @@
rules[ruleIndex].ip = [];
v.forEach(d => {
- rules[ruleIndex].ip.push("geoip:" + d);
+ rules[ruleIndex].ip.push(d);
});
}
this.allSetting.subJsonRules = JSON.stringify(rules);
@@ -778,34 +806,18 @@
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const domainRule = rules.find(r => r.domain);
- return domainRule?.domain.map(d => {
- if (d.startsWith("geosite:category-")) {
- return d.replace("geosite:category-", "");
- }
- return d.replace("geosite:", "");
- })
- ?? [];
+ return domainRule?.domain ?? [];
},
set: function (v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return;
-
if (v.length == 0) {
rules = rules.filter(r => !r.domain);
} else {
let ruleIndex = rules.findIndex(r => r.domain);
if (ruleIndex == -1) ruleIndex = rules.push(this.defaultRules[0]) - 1;
- rules[ruleIndex].domain = [];
- v.forEach(d => {
- let category = '';
- if (["cn", "apple", "meta", "google"].includes(d)) {
- category = "";
- } else if (["ru", "ir"].includes(d)) {
- category = "category-";
- }
- rules[ruleIndex].domain.push("geosite:" + category + d);
- });
+ rules[ruleIndex].domain = v;
}
this.allSetting.subJsonRules = JSON.stringify(rules);
}
@@ -838,4 +850,4 @@
});
|