mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-18 05:25:49 +00:00
feat: add bulk client management support and improve inbound data handling
This commit is contained in:
parent
97f284c9a7
commit
e7f2790236
12 changed files with 108 additions and 78 deletions
|
|
@ -581,7 +581,7 @@ prompt_and_setup_ssl() {
|
||||||
|
|
||||||
# 3.1 Request Domain to compose Panel URL later
|
# 3.1 Request Domain to compose Panel URL later
|
||||||
read -rp "Please enter domain name certificate issued for: " custom_domain
|
read -rp "Please enter domain name certificate issued for: " custom_domain
|
||||||
custom_domain="${custom_domain// /}" # Убираем пробелы
|
custom_domain="${custom_domain// /}" # Remove spaces
|
||||||
|
|
||||||
# 3.2 Loop for Certificate Path
|
# 3.2 Loop for Certificate Path
|
||||||
while true; do
|
while true; do
|
||||||
|
|
|
||||||
|
|
@ -609,7 +609,7 @@ prompt_and_setup_ssl() {
|
||||||
|
|
||||||
# 3.1 Request Domain to compose Panel URL later
|
# 3.1 Request Domain to compose Panel URL later
|
||||||
read -rp "Please enter domain name certificate issued for: " custom_domain
|
read -rp "Please enter domain name certificate issued for: " custom_domain
|
||||||
custom_domain="${custom_domain// /}" # Убираем пробелы
|
custom_domain="${custom_domain// /}" # Remove spaces
|
||||||
|
|
||||||
# 3.2 Loop for Certificate Path
|
# 3.2 Loop for Certificate Path
|
||||||
while true; do
|
while true; do
|
||||||
|
|
|
||||||
|
|
@ -91,6 +91,10 @@ class DBInbound {
|
||||||
}
|
}
|
||||||
|
|
||||||
toInbound() {
|
toInbound() {
|
||||||
|
if (this._cachedInbound) {
|
||||||
|
return this._cachedInbound;
|
||||||
|
}
|
||||||
|
|
||||||
let settings = {};
|
let settings = {};
|
||||||
if (!ObjectUtil.isEmpty(this.settings)) {
|
if (!ObjectUtil.isEmpty(this.settings)) {
|
||||||
settings = JSON.parse(this.settings);
|
settings = JSON.parse(this.settings);
|
||||||
|
|
@ -116,7 +120,21 @@ class DBInbound {
|
||||||
sniffing: sniffing,
|
sniffing: sniffing,
|
||||||
clientStats: this.clientStats,
|
clientStats: this.clientStats,
|
||||||
};
|
};
|
||||||
return Inbound.fromJson(config);
|
|
||||||
|
this._cachedInbound = Inbound.fromJson(config);
|
||||||
|
return this._cachedInbound;
|
||||||
|
}
|
||||||
|
|
||||||
|
getClientStats(email) {
|
||||||
|
if (!this._clientStatsMap) {
|
||||||
|
this._clientStatsMap = new Map();
|
||||||
|
if (this.clientStats && Array.isArray(this.clientStats)) {
|
||||||
|
for (const stats of this.clientStats) {
|
||||||
|
this._clientStatsMap.set(stats.email, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this._clientStatsMap.get(email);
|
||||||
}
|
}
|
||||||
|
|
||||||
isMultiUser() {
|
isMultiUser() {
|
||||||
|
|
|
||||||
|
|
@ -30,8 +30,10 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var upgrader = ws.Upgrader{
|
var upgrader = ws.Upgrader{
|
||||||
ReadBufferSize: 4096, // Increased from 1024 for better performance
|
ReadBufferSize: 32768, // Huge buffers for huge databases
|
||||||
WriteBufferSize: 4096, // Increased from 1024 for better performance
|
WriteBufferSize: 32768, // Huge buffers to reduce TCP fragmentation
|
||||||
|
EnableCompression: true, // Automatically GZIP large messages unconditionally
|
||||||
|
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
// Check origin for security
|
// Check origin for security
|
||||||
origin := r.Header.Get("Origin")
|
origin := r.Header.Get("Origin")
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<a-sidebar></a-sidebar>
|
<a-sidebar></a-sidebar>
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}' size="large">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||||
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||||
|
|
@ -14,10 +14,7 @@
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-row v-if="!loadingStates.fetched">
|
<a-row v-if="!loadingStates.fetched">
|
||||||
<a-card
|
<div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
|
||||||
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
|
||||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
|
||||||
</a-card>
|
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
||||||
<a-col>
|
<a-col>
|
||||||
|
|
@ -1304,7 +1301,6 @@
|
||||||
if (!clients || !Array.isArray(clients)) return;
|
if (!clients || !Array.isArray(clients)) return;
|
||||||
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
index = this.findIndexOfClient(dbInbound.protocol, clients, client);
|
||||||
if (index < 0 || !clients[index]) return;
|
if (index < 0 || !clients[index]) return;
|
||||||
clients[index].enable = !clients[index].enable;
|
|
||||||
clientId = this.getClientId(dbInbound.protocol, clients[index]);
|
clientId = this.getClientId(dbInbound.protocol, clients[index]);
|
||||||
await this.updateClient(clients[index], dbInboundId, clientId);
|
await this.updateClient(clients[index], dbInboundId, clientId);
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
|
|
@ -1317,7 +1313,7 @@
|
||||||
},
|
},
|
||||||
getInboundClients(dbInbound) {
|
getInboundClients(dbInbound) {
|
||||||
if (!dbInbound) return null;
|
if (!dbInbound) return null;
|
||||||
const inbound = dbInbound.toInbound();
|
const inbound = this.inbounds.find(ib => ib.id === dbInbound.id) || dbInbound.toInbound();
|
||||||
return inbound && inbound.clients ? inbound.clients : null;
|
return inbound && inbound.clients ? inbound.clients : null;
|
||||||
},
|
},
|
||||||
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
resetClientTraffic(client, dbInboundId, confirmation = true) {
|
||||||
|
|
@ -1367,42 +1363,54 @@
|
||||||
isExpiry(dbInbound, index) {
|
isExpiry(dbInbound, index) {
|
||||||
return dbInbound.toInbound().isExpiry(index);
|
return dbInbound.toInbound().isExpiry(index);
|
||||||
},
|
},
|
||||||
|
getClientStats(dbInbound, email) {
|
||||||
|
if (!dbInbound) return null;
|
||||||
|
if (!dbInbound._clientStatsMap) {
|
||||||
|
dbInbound._clientStatsMap = new Map();
|
||||||
|
if (dbInbound.clientStats && Array.isArray(dbInbound.clientStats)) {
|
||||||
|
for (const stats of dbInbound.clientStats) {
|
||||||
|
dbInbound._clientStatsMap.set(stats.email, stats);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dbInbound._clientStatsMap.get(email);
|
||||||
|
},
|
||||||
getUpStats(dbInbound, email) {
|
getUpStats(dbInbound, email) {
|
||||||
if (email.length == 0) return 0;
|
if (!email || email.length == 0) return 0;
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
return clientStats ? clientStats.up : 0;
|
return clientStats ? clientStats.up : 0;
|
||||||
},
|
},
|
||||||
getDownStats(dbInbound, email) {
|
getDownStats(dbInbound, email) {
|
||||||
if (email.length == 0) return 0;
|
if (!email || email.length == 0) return 0;
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
return clientStats ? clientStats.down : 0;
|
return clientStats ? clientStats.down : 0;
|
||||||
},
|
},
|
||||||
getSumStats(dbInbound, email) {
|
getSumStats(dbInbound, email) {
|
||||||
if (email.length == 0) return 0;
|
if (!email || email.length == 0) return 0;
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
return clientStats ? clientStats.up + clientStats.down : 0;
|
return clientStats ? clientStats.up + clientStats.down : 0;
|
||||||
},
|
},
|
||||||
getAllTimeClient(dbInbound, email) {
|
getAllTimeClient(dbInbound, email) {
|
||||||
if (email.length == 0) return 0;
|
if (!email || email.length == 0) return 0;
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
if (!clientStats) return 0;
|
if (!clientStats) return 0;
|
||||||
return clientStats.allTime || (clientStats.up + clientStats.down);
|
return clientStats.allTime || (clientStats.up + clientStats.down);
|
||||||
},
|
},
|
||||||
getRemStats(dbInbound, email) {
|
getRemStats(dbInbound, email) {
|
||||||
if (email.length == 0) return 0;
|
if (!email || email.length == 0) return 0;
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
if (!clientStats) return 0;
|
if (!clientStats) return 0;
|
||||||
remained = clientStats.total - (clientStats.up + clientStats.down);
|
let remained = clientStats.total - (clientStats.up + clientStats.down);
|
||||||
return remained > 0 ? remained : 0;
|
return remained > 0 ? remained : 0;
|
||||||
},
|
},
|
||||||
clientStatsColor(dbInbound, email) {
|
clientStatsColor(dbInbound, email) {
|
||||||
if (email.length == 0) return ColorUtils.clientUsageColor();
|
if (!email || email.length == 0) return ColorUtils.clientUsageColor();
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
|
return ColorUtils.clientUsageColor(clientStats, app.trafficDiff)
|
||||||
},
|
},
|
||||||
statsProgress(dbInbound, email) {
|
statsProgress(dbInbound, email) {
|
||||||
if (email.length == 0) return 100;
|
if (!email || email.length == 0) return 100;
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
if (!clientStats) return 0;
|
if (!clientStats) return 0;
|
||||||
if (clientStats.total == 0) return 100;
|
if (clientStats.total == 0) return 100;
|
||||||
return 100 * (clientStats.down + clientStats.up) / clientStats.total;
|
return 100 * (clientStats.down + clientStats.up) / clientStats.total;
|
||||||
|
|
@ -1415,11 +1423,11 @@
|
||||||
return 100 * (1 - (remainedSeconds / resetSeconds));
|
return 100 * (1 - (remainedSeconds / resetSeconds));
|
||||||
},
|
},
|
||||||
statsExpColor(dbInbound, email) {
|
statsExpColor(dbInbound, email) {
|
||||||
if (email.length == 0) return '#7a316f';
|
if (!email || email.length == 0) return '#7a316f';
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
let clientStats = this.getClientStats(dbInbound, email);
|
||||||
if (!clientStats) return '#7a316f';
|
if (!clientStats) return '#7a316f';
|
||||||
statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
|
let statsColor = ColorUtils.usageColor(clientStats.down + clientStats.up, this.trafficDiff, clientStats.total);
|
||||||
expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
|
let expColor = ColorUtils.usageColor(new Date().getTime(), this.expireDiff, clientStats.expiryTime);
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case statsColor == "red" || expColor == "red":
|
case statsColor == "red" || expColor == "red":
|
||||||
return "#cf3c3c"; // Red
|
return "#cf3c3c"; // Red
|
||||||
|
|
@ -1432,12 +1440,12 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
isClientEnabled(dbInbound, email) {
|
isClientEnabled(dbInbound, email) {
|
||||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
|
let clientStats = dbInbound ? this.getClientStats(dbInbound, email) : null;
|
||||||
return clientStats ? clientStats['enable'] : true;
|
return clientStats ? clientStats['enable'] : true;
|
||||||
},
|
},
|
||||||
isClientDepleted(dbInbound, email) {
|
isClientDepleted(dbInbound, email) {
|
||||||
if (!email || !dbInbound || !dbInbound.clientStats) return false;
|
if (!email || !dbInbound) return false;
|
||||||
const stats = dbInbound.clientStats.find(s => s.email === email);
|
const stats = this.getClientStats(dbInbound, email);
|
||||||
if (!stats) return false;
|
if (!stats) return false;
|
||||||
const total = stats.total ?? 0;
|
const total = stats.total ?? 0;
|
||||||
const used = (stats.up ?? 0) + (stats.down ?? 0);
|
const used = (stats.up ?? 0) + (stats.down ?? 0);
|
||||||
|
|
@ -1557,12 +1565,18 @@
|
||||||
pagination(obj) {
|
pagination(obj) {
|
||||||
if (this.pageSize > 0 && obj.length > this.pageSize) {
|
if (this.pageSize > 0 && obj.length > this.pageSize) {
|
||||||
// Set page options based on object size
|
// Set page options based on object size
|
||||||
sizeOptions = [];
|
let sizeOptions = [this.pageSize.toString()];
|
||||||
for (i = this.pageSize; i <= obj.length; i = i + this.pageSize) {
|
const increments = [2, 5, 10, 20];
|
||||||
sizeOptions.push(i.toString());
|
for (const m of increments) {
|
||||||
|
const val = this.pageSize * m;
|
||||||
|
if (val < obj.length && val <= 1000) {
|
||||||
|
sizeOptions.push(val.toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Add option to see all in one page
|
// Add option to see all in one page
|
||||||
sizeOptions.push(i.toString());
|
if (!sizeOptions.includes(obj.length.toString())) {
|
||||||
|
sizeOptions.push(obj.length.toString());
|
||||||
|
}
|
||||||
|
|
||||||
p = {
|
p = {
|
||||||
showSizeChanger: true,
|
showSizeChanger: true,
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<a-sidebar></a-sidebar>
|
<a-sidebar></a-sidebar>
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
|
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip" size="large">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
|
<a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
|
||||||
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable>
|
||||||
|
|
@ -15,9 +15,7 @@
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<template>
|
<template>
|
||||||
<a-row v-if="!loadingStates.fetched">
|
<a-row v-if="!loadingStates.fetched">
|
||||||
<a-card class="card-placeholder text-center">
|
<div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
|
||||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
|
||||||
</a-card>
|
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
||||||
<a-col>
|
<a-col>
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
|
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
|
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
|
||||||
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="10000"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
|
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
|
||||||
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
|
|
@ -204,7 +204,7 @@
|
||||||
this.security = "auto";
|
this.security = "auto";
|
||||||
this.flow = "";
|
this.flow = "";
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = Inbound.fromJson(dbInbound.toInbound().toJson());
|
||||||
this.delayedStart = false;
|
this.delayedStart = false;
|
||||||
this.reset = 0;
|
this.reset = 0;
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@
|
||||||
this.okText = okText;
|
this.okText = okText;
|
||||||
this.isEdit = isEdit;
|
this.isEdit = isEdit;
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = Inbound.fromJson(dbInbound.toInbound().toJson());
|
||||||
this.clients = this.inbound.clients;
|
this.clients = this.inbound.clients;
|
||||||
this.index = index === null ? this.clients.length : index;
|
this.index = index === null ? this.clients.length : index;
|
||||||
this.delayedStart = false;
|
this.delayedStart = false;
|
||||||
|
|
@ -98,9 +98,9 @@
|
||||||
return app.datepicker;
|
return app.datepicker;
|
||||||
},
|
},
|
||||||
get isTrafficExhausted() {
|
get isTrafficExhausted() {
|
||||||
if (!clientStats) return false
|
if (!this.clientStats) return false
|
||||||
if (clientStats.total <= 0) return false
|
if (this.clientStats.total <= 0) return false
|
||||||
if (clientStats.up + clientStats.down < clientStats.total) return false
|
if (this.clientStats.up + this.clientStats.down < this.clientStats.total) return false
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
get isExpiry() {
|
get isExpiry() {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
<a-sidebar></a-sidebar>
|
<a-sidebar></a-sidebar>
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}' size="large">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||||
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
|
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
|
||||||
|
|
@ -21,10 +21,7 @@
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<template>
|
<template>
|
||||||
<a-row v-if="!loadingStates.fetched">
|
<a-row v-if="!loadingStates.fetched">
|
||||||
<a-card
|
<div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
|
||||||
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
|
||||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
|
||||||
</a-card>
|
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
||||||
<a-col>
|
<a-col>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="500"
|
<a-spin :spinning="loadingStates.spinning" :delay="500"
|
||||||
tip='{{ i18n "loading"}}'>
|
tip='{{ i18n "loading"}}' size="large">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched"
|
<a-alert type="error" v-if="showAlert && loadingStates.fetched"
|
||||||
:style="{ marginBottom: '10px' }"
|
:style="{ marginBottom: '10px' }"
|
||||||
|
|
@ -24,10 +24,7 @@
|
||||||
</transition>
|
</transition>
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-row v-if="!loadingStates.fetched">
|
<a-row v-if="!loadingStates.fetched">
|
||||||
<a-card
|
<div :style="{ minHeight: 'calc(100vh - 120px)' }"></div>
|
||||||
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
|
||||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
|
||||||
</a-card>
|
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
<a-row :gutter="[isMobile ? 8 : 16, isMobile ? 0 : 12]" v-else>
|
||||||
<a-col>
|
<a-col>
|
||||||
|
|
|
||||||
|
|
@ -118,31 +118,35 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
clients, ok := settings["clients"].([]any)
|
clients, ok := settings["clients"].([]any)
|
||||||
if ok {
|
if ok {
|
||||||
// check users active or not
|
// Fast O(N) lookup map for client traffic enablement
|
||||||
clientStats := inbound.ClientStats
|
clientStats := inbound.ClientStats
|
||||||
|
enableMap := make(map[string]bool, len(clientStats))
|
||||||
for _, clientTraffic := range clientStats {
|
for _, clientTraffic := range clientStats {
|
||||||
indexDecrease := 0
|
enableMap[clientTraffic.Email] = clientTraffic.Enable
|
||||||
for index, client := range clients {
|
|
||||||
c := client.(map[string]any)
|
|
||||||
if c["email"] == clientTraffic.Email {
|
|
||||||
if !clientTraffic.Enable {
|
|
||||||
clients = RemoveIndex(clients, index-indexDecrease)
|
|
||||||
indexDecrease++
|
|
||||||
logger.Infof("Remove Inbound User %s due to expiration or traffic limit", c["email"])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear client config for additional parameters
|
// filter and clean clients
|
||||||
var final_clients []any
|
var final_clients []any
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
c := client.(map[string]any)
|
c, ok := client.(map[string]any)
|
||||||
if c["enable"] != nil {
|
if !ok {
|
||||||
if enable, ok := c["enable"].(bool); ok && !enable {
|
continue
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
email, _ := c["email"].(string)
|
||||||
|
|
||||||
|
// check users active or not via stats
|
||||||
|
if enable, exists := enableMap[email]; exists && !enable {
|
||||||
|
logger.Infof("Remove Inbound User %s due to expiration or traffic limit", email)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// check manual disabled flag
|
||||||
|
if manualEnable, ok := c["enable"].(bool); ok && !manualEnable {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear client config for additional parameters
|
||||||
for key := range c {
|
for key := range c {
|
||||||
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
|
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "method" {
|
||||||
delete(c, key)
|
delete(c, key)
|
||||||
|
|
|
||||||
|
|
@ -279,7 +279,7 @@ func (h *Hub) Broadcast(messageType MessageType, payload any) {
|
||||||
|
|
||||||
// If message exceeds size limit, send a lightweight invalidate notification
|
// If message exceeds size limit, send a lightweight invalidate notification
|
||||||
// instead of dropping it entirely — the frontend will re-fetch via REST API
|
// instead of dropping it entirely — the frontend will re-fetch via REST API
|
||||||
const maxMessageSize = 1024 * 1024 // 1MB
|
const maxMessageSize = 100 * 1024 * 1024 // 100MB (User override)
|
||||||
if len(data) > maxMessageSize {
|
if len(data) > maxMessageSize {
|
||||||
logger.Debugf("WebSocket message too large (%d bytes) for type %s, sending invalidate signal", len(data), messageType)
|
logger.Debugf("WebSocket message too large (%d bytes) for type %s, sending invalidate signal", len(data), messageType)
|
||||||
h.broadcastInvalidate(messageType)
|
h.broadcastInvalidate(messageType)
|
||||||
|
|
@ -324,7 +324,7 @@ func (h *Hub) BroadcastToTopic(messageType MessageType, payload any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// If message exceeds size limit, send a lightweight invalidate notification
|
// If message exceeds size limit, send a lightweight invalidate notification
|
||||||
const maxMessageSize = 1024 * 1024 // 1MB
|
const maxMessageSize = 100 * 1024 * 1024 // 100MB (User override)
|
||||||
if len(data) > maxMessageSize {
|
if len(data) > maxMessageSize {
|
||||||
logger.Debugf("WebSocket message too large (%d bytes) for type %s, sending invalidate signal", len(data), messageType)
|
logger.Debugf("WebSocket message too large (%d bytes) for type %s, sending invalidate signal", len(data), messageType)
|
||||||
h.broadcastInvalidate(messageType)
|
h.broadcastInvalidate(messageType)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue