add hysteria inbound
Some checks are pending
CodeQL Advanced / Analyze (go) (push) Waiting to run
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Release 3X-UI / Analyze Go code (push) Waiting to run
Release 3X-UI / build (386) (push) Blocked by required conditions
Release 3X-UI / build (amd64) (push) Blocked by required conditions
Release 3X-UI / build (arm64) (push) Blocked by required conditions
Release 3X-UI / build (armv5) (push) Blocked by required conditions
Release 3X-UI / build (armv6) (push) Blocked by required conditions
Release 3X-UI / build (armv7) (push) Blocked by required conditions
Release 3X-UI / build (s390x) (push) Blocked by required conditions
Release 3X-UI / Build for Windows (push) Blocked by required conditions

Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
This commit is contained in:
MHSanaei 2026-04-20 16:05:27 +02:00
parent c188056f64
commit ae5ad505d0
No known key found for this signature in database
GPG key ID: 7E4060F2FBE5AB7A
19 changed files with 1081 additions and 511 deletions

View file

@ -21,6 +21,7 @@ const (
Shadowsocks Protocol = "shadowsocks" Shadowsocks Protocol = "shadowsocks"
Mixed Protocol = "mixed" Mixed Protocol = "mixed"
WireGuard Protocol = "wireguard" WireGuard Protocol = "wireguard"
Hysteria Protocol = "hysteria"
) )
// User represents a user account in the 3x-ui panel. // User represents a user account in the 3x-ui panel.
@ -118,10 +119,11 @@ type CustomGeoResource struct {
// Client represents a client configuration for Xray inbounds with traffic limits and settings. // Client represents a client configuration for Xray inbounds with traffic limits and settings.
type Client struct { type Client struct {
ID string `json:"id"` // Unique client identifier ID string `json:"id,omitempty"` // Unique client identifier
Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm") Security string `json:"security"` // Security method (e.g., "auto", "aes-128-gcm")
Password string `json:"password"` // Client password Password string `json:"password,omitempty"` // Client password
Flow string `json:"flow"` // Flow control (XTLS) Flow string `json:"flow,omitempty"` // Flow control (XTLS)
Auth string `json:"auth,omitempty"` // Auth password (Hysteria)
Email string `json:"email"` // Client email identifier Email string `json:"email"` // Client email identifier
LimitIP int `json:"limitIp"` // IP limit for this client LimitIP int `json:"limitIp"` // IP limit for this client
TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB TotalGB int64 `json:"totalGB" form:"totalGB"` // Total traffic limit in GB

View file

@ -194,6 +194,8 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client)) newOutbounds = append(newOutbounds, s.genVless(inbound, streamSettings, client))
case "trojan", "shadowsocks": case "trojan", "shadowsocks":
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client)) newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
case "hysteria":
newOutbounds = append(newOutbounds, s.genHy(inbound, newStream, client))
} }
newOutbounds = append(newOutbounds, s.defaultOutbounds...) newOutbounds = append(newOutbounds, s.defaultOutbounds...)
@ -389,6 +391,49 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
return result return result
} }
func (s *SubJsonService) genHy(inbound *model.Inbound, newStream map[string]any, client model.Client) json_util.RawMessage {
outbound := Outbound{}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = "proxy"
if s.mux != "" {
outbound.Mux = json_util.RawMessage(s.mux)
}
var settings, stream map[string]any
json.Unmarshal([]byte(inbound.Settings), &settings)
version, _ := settings["version"].(float64)
outbound.Settings = map[string]any{
"version": int(version),
"address": inbound.Listen,
"port": inbound.Port,
}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
hyStream := stream["hysteriaSettings"].(map[string]any)
outHyStream := map[string]any{
"version": int(version),
"auth": client.Auth,
}
if udpIdleTimeout, ok := hyStream["udpIdleTimeout"].(float64); ok {
outHyStream["udpIdleTimeout"] = int(udpIdleTimeout)
}
newStream["hysteriaSettings"] = outHyStream
if finalmask, ok := hyStream["finalmask"].(map[string]any); ok {
newStream["finalmask"] = finalmask
}
newStream["network"] = "hysteria"
newStream["security"] = "tls"
outbound.StreamSettings, _ = json.MarshalIndent(newStream, "", " ")
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
type Outbound struct { type Outbound struct {
Protocol string `json:"protocol"` Protocol string `json:"protocol"`
Tag string `json:"tag"` Tag string `json:"tag"`

View file

@ -120,7 +120,7 @@ func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error)
FROM inbounds, FROM inbounds,
JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client JSON_EACH(JSON_EXTRACT(inbounds.settings, '$.clients')) AS client
WHERE WHERE
protocol in ('vmess','vless','trojan','shadowsocks') protocol in ('vmess','vless','trojan','shadowsocks','hysteria')
AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ? AND JSON_EXTRACT(client.value, '$.subId') = ? AND enable = ?
)`, subId, true).Find(&inbounds).Error )`, subId, true).Find(&inbounds).Error
if err != nil { if err != nil {
@ -171,6 +171,8 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
return s.genTrojanLink(inbound, email) return s.genTrojanLink(inbound, email)
case "shadowsocks": case "shadowsocks":
return s.genShadowsocksLink(inbound, email) return s.genShadowsocksLink(inbound, email)
case "hysteria":
return s.genHysteriaLink(inbound, email)
} }
return "" return ""
} }
@ -885,6 +887,70 @@ func (s *SubService) genShadowsocksLink(inbound *model.Inbound, email string) st
return url.String() return url.String()
} }
func (s *SubService) genHysteriaLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.Hysteria {
return ""
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
clients, _ := s.inboundService.GetClients(inbound)
clientIndex := -1
for i, client := range clients {
if client.Email == email {
clientIndex = i
break
}
}
auth := clients[clientIndex].Auth
port := inbound.Port
params := make(map[string]string)
params["security"] = "tls"
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
if len(alpn) > 0 {
params["alpn"] = strings.Join(alpn, ",")
}
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
if insecure.(bool) {
params["insecure"] = "1"
}
}
}
var settings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &settings)
version, _ := settings["version"].(float64)
protocol := "hysteria2"
if int(version) == 1 {
protocol = "hysteria"
}
link := fmt.Sprintf("%s://%s@%s:%d", protocol, auth, address, port)
url, _ := url.Parse(link)
q := url.Query()
for k, v := range params {
q.Add(k, v)
}
url.RawQuery = q.Encode()
url.Fragment = s.genRemark(inbound, email, "")
return url.String()
}
func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string { func (s *SubService) genRemark(inbound *model.Inbound, email string, extra string) string {
separationChar := string(s.remarkModel[0]) separationChar := string(s.remarkModel[0])
orderChars := s.remarkModel[1:] orderChars := s.remarkModel[1:]

View file

@ -125,7 +125,7 @@ class DBInbound {
sniffing: sniffing, sniffing: sniffing,
clientStats: this.clientStats, clientStats: this.clientStats,
}; };
this._cachedInbound = Inbound.fromJson(config); this._cachedInbound = Inbound.fromJson(config);
return this._cachedInbound; return this._cachedInbound;
} }
@ -147,6 +147,7 @@ class DBInbound {
case Protocols.VMESS: case Protocols.VMESS:
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.HYSTERIA:
return true; return true;
case Protocols.SHADOWSOCKS: case Protocols.SHADOWSOCKS:
return this.toInbound().isSSMultiUser; return this.toInbound().isSSMultiUser;
@ -161,6 +162,7 @@ class DBInbound {
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
case Protocols.SHADOWSOCKS: case Protocols.SHADOWSOCKS:
case Protocols.HYSTERIA:
return true; return true;
default: default:
return false; return false;

View file

@ -8,6 +8,7 @@ const Protocols = {
HTTP: 'http', HTTP: 'http',
WIREGUARD: 'wireguard', WIREGUARD: 'wireguard',
TUN: 'tun', TUN: 'tun',
HYSTERIA: 'hysteria',
}; };
const SSMethods = { const SSMethods = {
@ -589,6 +590,106 @@ class xHTTPStreamSettings extends XrayCommonClass {
} }
} }
class HysteriaStreamSettings extends XrayCommonClass {
constructor(
protocol,
version = 2,
auth = '',
udpIdleTimeout = 60,
masquerade,
) {
super(protocol);
this.version = version;
this.auth = auth;
this.udpIdleTimeout = udpIdleTimeout;
this.masquerade = masquerade;
}
static fromJson(json = {}) {
return new HysteriaStreamSettings(
json.protocol,
json.version ?? 2,
json.auth ?? '',
json.udpIdleTimeout ?? 60,
json.masquerade ? HysteriaMasquerade.fromJson(json.masquerade) : undefined,
);
}
toJson() {
return {
protocol: this.protocol,
version: this.version,
auth: this.auth,
udpIdleTimeout: this.udpIdleTimeout,
masquerade: this.masqueradeSwitch ? this.masquerade.toJson() : undefined,
};
}
get masqueradeSwitch() {
return this.masquerade != undefined;
}
set masqueradeSwitch(value) {
this.masquerade = value ? new HysteriaMasquerade() : undefined;
}
};
class HysteriaMasquerade extends XrayCommonClass {
constructor(
type = 'proxy',
dir = '',
url = '',
rewriteHost = false,
insecure = false,
content = '',
headers = [],
statusCode = 0,
) {
super();
this.type = type;
this.dir = dir;
this.url = url;
this.rewriteHost = rewriteHost;
this.insecure = insecure;
this.content = content;
this.headers = headers;
this.statusCode = statusCode;
}
addHeader(name, value) {
this.headers.push({ name: name, value: value });
}
removeHeader(index) {
this.headers.splice(index, 1);
}
static fromJson(json = {}) {
return new HysteriaMasquerade(
json.type,
json.dir,
json.url,
json.rewriteHost,
json.insecure,
json.content,
XrayCommonClass.toHeaders(json.headers),
json.statusCode,
);
}
toJson() {
return {
type: this.type,
dir: this.dir,
url: this.url,
rewriteHost: this.rewriteHost,
insecure: this.insecure,
content: this.content,
headers: XrayCommonClass.toV2Headers(this.headers, false),
statusCode: this.statusCode,
};
}
};
class TlsStreamSettings extends XrayCommonClass { class TlsStreamSettings extends XrayCommonClass {
constructor( constructor(
serverName = '', serverName = '',
@ -987,6 +1088,12 @@ class UdpMask extends XrayCommonClass {
case 'header-wechat': case 'header-wechat':
case 'header-wireguard': case 'header-wireguard':
return {}; return {};
case 'header-custom':
return { client: [], server: [] };
case 'noise':
return { reset: 0, noise: [] };
case 'sudoku':
return { ascii: '', customTable: '', customTables: [], paddingMin: 0, paddingMax: 0 };
default: default:
return settings; return settings;
} }
@ -1021,7 +1128,6 @@ class FinalMaskStreamSettings extends XrayCommonClass {
return { return {
udp: this.udp.map(udp => udp.toJson()) udp: this.udp.map(udp => udp.toJson())
}; };
} }
} }
@ -1037,6 +1143,7 @@ class StreamSettings extends XrayCommonClass {
grpcSettings = new GrpcStreamSettings(), grpcSettings = new GrpcStreamSettings(),
httpupgradeSettings = new HTTPUpgradeStreamSettings(), httpupgradeSettings = new HTTPUpgradeStreamSettings(),
xhttpSettings = new xHTTPStreamSettings(), xhttpSettings = new xHTTPStreamSettings(),
hysteriaSettings = new HysteriaStreamSettings(),
finalmask = new FinalMaskStreamSettings(), finalmask = new FinalMaskStreamSettings(),
sockopt = undefined, sockopt = undefined,
) { ) {
@ -1052,6 +1159,7 @@ class StreamSettings extends XrayCommonClass {
this.grpc = grpcSettings; this.grpc = grpcSettings;
this.httpupgrade = httpupgradeSettings; this.httpupgrade = httpupgradeSettings;
this.xhttp = xhttpSettings; this.xhttp = xhttpSettings;
this.hysteria = hysteriaSettings;
this.finalmask = finalmask; this.finalmask = finalmask;
this.sockopt = sockopt; this.sockopt = sockopt;
} }
@ -1116,6 +1224,7 @@ class StreamSettings extends XrayCommonClass {
GrpcStreamSettings.fromJson(json.grpcSettings), GrpcStreamSettings.fromJson(json.grpcSettings),
HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings), HTTPUpgradeStreamSettings.fromJson(json.httpupgradeSettings),
xHTTPStreamSettings.fromJson(json.xhttpSettings), xHTTPStreamSettings.fromJson(json.xhttpSettings),
HysteriaStreamSettings.fromJson(json.hysteriaSettings),
FinalMaskStreamSettings.fromJson(json.finalmask), FinalMaskStreamSettings.fromJson(json.finalmask),
SockoptStreamSettings.fromJson(json.sockopt), SockoptStreamSettings.fromJson(json.sockopt),
); );
@ -1135,6 +1244,7 @@ class StreamSettings extends XrayCommonClass {
grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined, grpcSettings: network === 'grpc' ? this.grpc.toJson() : undefined,
httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined, httpupgradeSettings: network === 'httpupgrade' ? this.httpupgrade.toJson() : undefined,
xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined, xhttpSettings: network === 'xhttp' ? this.xhttp.toJson() : undefined,
hysteriaSettings: network === 'hysteria' ? this.hysteria.toJson() : undefined,
finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined, finalmask: this.hasFinalMask ? this.finalmask.toJson() : undefined,
sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined, sockopt: this.sockopt != undefined ? this.sockopt.toJson() : undefined,
}; };
@ -1201,6 +1311,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VLESS: return this.settings.vlesses; case Protocols.VLESS: return this.settings.vlesses;
case Protocols.TROJAN: return this.settings.trojans; case Protocols.TROJAN: return this.settings.trojans;
case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null; case Protocols.SHADOWSOCKS: return this.isSSMultiUser ? this.settings.shadowsockses : null;
case Protocols.HYSTERIA: return this.settings.hysterias;
default: return null; default: return null;
} }
} }
@ -1212,9 +1323,14 @@ class Inbound extends XrayCommonClass {
set protocol(protocol) { set protocol(protocol) {
this._protocol = protocol; this._protocol = protocol;
this.settings = Inbound.Settings.getSettings(protocol); this.settings = Inbound.Settings.getSettings(protocol);
this.stream = new StreamSettings();
if (protocol === Protocols.TROJAN) { if (protocol === Protocols.TROJAN) {
this.tls = false; this.tls = false;
} }
if (protocol === Protocols.HYSTERIA) {
this.stream.network = 'hysteria';
this.stream.security = 'tls';
}
} }
get network() { get network() {
@ -1316,6 +1432,7 @@ class Inbound extends XrayCommonClass {
} }
canEnableTls() { canEnableTls() {
if (this.protocol === Protocols.HYSTERIA) return true;
if (![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false; if (![Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.network); return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.network);
} }
@ -1342,7 +1459,7 @@ class Inbound extends XrayCommonClass {
} }
canEnableStream() { canEnableStream() {
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol); return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS, Protocols.HYSTERIA].includes(this.protocol);
} }
reset() { reset() {
@ -1689,6 +1806,26 @@ class Inbound extends XrayCommonClass {
return url.toString(); return url.toString();
} }
genHysteriaLink(address = '', port = this.port, remark = '', clientAuth) {
const protocol = this.settings.version == 2 ? "hysteria2" : "hysteria";
const link = `${protocol}://${clientAuth}@${address}:${port}`;
const params = new Map();
params.set("security", "tls");
if (this.stream.tls.settings.fingerprint?.length > 0) params.set("fp", this.stream.tls.settings.fingerprint);
if (this.stream.tls.alpn?.length > 0) params.set("alpn", this.stream.tls.alpn);
if (this.stream.tls.settings.allowInsecure) params.set("insecure", "1");
if (this.stream.tls.settings.echConfigList?.length > 0) params.set("ech", this.stream.tls.settings.echConfigList.join(','));
if (this.stream.tls.sni?.length > 0) params.set("sni", this.stream.tls.sni);
const url = new URL(link);
for (const [key, value] of params) {
url.searchParams.set(key, value);
}
url.hash = encodeURIComponent(remark);
return url.toString();
}
getWireguardLink(address, port, remark, peerId) { getWireguardLink(address, port, remark, peerId) {
let txt = `[Interface]\n` let txt = `[Interface]\n`
txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n` txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n`
@ -1721,6 +1858,8 @@ class Inbound extends XrayCommonClass {
return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : ''); return this.genSSLink(address, port, forceTls, remark, this.isSSMultiUser ? client.password : '');
case Protocols.TROJAN: case Protocols.TROJAN:
return this.genTrojanLink(address, port, forceTls, remark, client.password); return this.genTrojanLink(address, port, forceTls, remark, client.password);
case Protocols.HYSTERIA:
return this.genHysteriaLink(address, port, remark, client.auth.length > 0 ? client.auth : this.stream.hysteria.auth);
default: return ''; default: return '';
} }
} }
@ -1827,6 +1966,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.HTTP: return new Inbound.HttpSettings(protocol); case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol); case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
case Protocols.TUN: return new Inbound.TunSettings(protocol); case Protocols.TUN: return new Inbound.TunSettings(protocol);
case Protocols.HYSTERIA: return new Inbound.HysteriaSettings(protocol);
default: return null; default: return null;
} }
} }
@ -1842,6 +1982,7 @@ Inbound.Settings = class extends XrayCommonClass {
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json); case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json); case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
case Protocols.TUN: return Inbound.TunSettings.fromJson(json); case Protocols.TUN: return Inbound.TunSettings.fromJson(json);
case Protocols.HYSTERIA: return Inbound.HysteriaSettings.fromJson(json);
default: return null; default: return null;
} }
} }
@ -1851,6 +1992,94 @@ Inbound.Settings = class extends XrayCommonClass {
} }
}; };
/** Shared user-quota fields and UI helpers for multi-user protocol clients. */
Inbound.ClientBase = class extends XrayCommonClass {
constructor(
email = RandomUtil.randomLowerAndNum(8),
limitIp = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0,
created_at = undefined,
updated_at = undefined,
) {
super();
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
}
static commonArgsFromJson(json = {}) {
return [
json.email,
json.limitIp,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
];
}
_clientBaseToJson() {
return {
email: this.email,
limitIp: this.limitIp,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
comment: this.comment,
reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
};
}
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === '') {
return null;
}
if (this.expiryTime < 0) {
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null || t === '') {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
};
Inbound.VmessSettings = class extends Inbound.Settings { Inbound.VmessSettings = class extends Inbound.Settings {
constructor(protocol, constructor(protocol,
vmesses = [new Inbound.VmessSettings.VMESS()]) { vmesses = [new Inbound.VmessSettings.VMESS()]) {
@ -1879,7 +2108,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.VmessSettings( return new Inbound.VmessSettings(
Protocols.VMESS, Protocols.VMESS,
json.clients.map(client => Inbound.VmessSettings.VMESS.fromJson(client)), (json.clients || []).map(client => Inbound.VmessSettings.VMESS.fromJson(client)),
); );
} }
@ -1890,80 +2119,32 @@ Inbound.VmessSettings = class extends Inbound.Settings {
} }
}; };
Inbound.VmessSettings.VMESS = class extends XrayCommonClass { Inbound.VmessSettings.VMESS = class extends Inbound.ClientBase {
constructor( constructor(
id = RandomUtil.randomUUID(), id = RandomUtil.randomUUID(),
security = USERS_SECURITY.AUTO, security = USERS_SECURITY.AUTO,
email = RandomUtil.randomLowerAndNum(8), email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
limitIp = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.id = id; this.id = id;
this.security = security; this.security = security;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.VmessSettings.VMESS( return new Inbound.VmessSettings.VMESS(
json.id, json.id,
json.security, json.security,
json.email, ...Inbound.ClientBase.commonArgsFromJson(json),
json.limitIp,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
); );
} }
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0) {
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
set _expiryTime(t) { toJson() {
if (t == null || t === "") { return {
this.expiryTime = 0; id: this.id,
} else { security: this.security,
this.expiryTime = t.valueOf(); ...this._clientBaseToJson(),
} };
} }
get _totalGB() {
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
}; };
Inbound.VLESSSettings = class extends Inbound.Settings { Inbound.VLESSSettings = class extends Inbound.Settings {
@ -2041,85 +2222,36 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
return json; return json;
} }
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { Inbound.VLESSSettings.VLESS = class extends Inbound.ClientBase {
constructor( constructor(
id = RandomUtil.randomUUID(), id = RandomUtil.randomUUID(),
flow = '', flow = '',
email = RandomUtil.randomLowerAndNum(8), email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
limitIp = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.id = id; this.id = id;
this.flow = flow; this.flow = flow;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.VLESSSettings.VLESS( return new Inbound.VLESSSettings.VLESS(
json.id, json.id,
json.flow, json.flow,
json.email, ...Inbound.ClientBase.commonArgsFromJson(json),
json.limitIp,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
); );
} }
get _expiryTime() { toJson() {
if (this.expiryTime === 0 || this.expiryTime === "") { return {
return null; id: this.id,
} flow: this.flow,
if (this.expiryTime < 0) { ...this._clientBaseToJson(),
return this.expiryTime / -86400000; };
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null || t === "") {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
} }
}; };
Inbound.VLESSSettings.Fallback = class extends XrayCommonClass { Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
constructor(name = "", alpn = '', path = '', dest = '', xver = 0) { constructor(name = "", alpn = '', path = '', dest = '', xver = 0) {
super(); super();
@ -2179,7 +2311,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.TrojanSettings( return new Inbound.TrojanSettings(
Protocols.TROJAN, Protocols.TROJAN,
json.clients.map(client => Inbound.TrojanSettings.Trojan.fromJson(client)), (json.clients || []).map(client => Inbound.TrojanSettings.Trojan.fromJson(client)),
Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),); Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
} }
@ -2191,95 +2323,28 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
} }
}; };
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { Inbound.TrojanSettings.Trojan = class extends Inbound.ClientBase {
constructor( constructor(
password = RandomUtil.randomSeq(10), password = RandomUtil.randomSeq(10),
email = RandomUtil.randomLowerAndNum(8), email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
limitIp = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.password = password; this.password = password;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
toJson() { toJson() {
return { return {
password: this.password, password: this.password,
email: this.email, ...this._clientBaseToJson(),
limitIp: this.limitIp,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
comment: this.comment,
reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
}; };
} }
static fromJson(json = {}) { static fromJson(json = {}) {
return new Inbound.TrojanSettings.Trojan( return new Inbound.TrojanSettings.Trojan(
json.password, json.password,
json.email, ...Inbound.ClientBase.commonArgsFromJson(json),
json.limitIp,
json.totalGB,
json.expiryTime,
json.enable,
json.tgId,
json.subId,
json.comment,
json.reset,
json.created_at,
json.updated_at,
); );
} }
get _expiryTime() {
if (this.expiryTime === 0 || this.expiryTime === "") {
return null;
}
if (this.expiryTime < 0) {
return this.expiryTime / -86400000;
}
return moment(this.expiryTime);
}
set _expiryTime(t) {
if (t == null || t === "") {
this.expiryTime = 0;
} else {
this.expiryTime = t.valueOf();
}
}
get _totalGB() {
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
}
set _totalGB(gb) {
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0);
}
}; };
Inbound.TrojanSettings.Fallback = class extends XrayCommonClass { Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
@ -2343,7 +2408,7 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
json.method, json.method,
json.password, json.password,
json.network, json.network,
json.clients.map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)), (json.clients || []).map(client => Inbound.ShadowsocksSettings.Shadowsocks.fromJson(client)),
json.ivCheck, json.ivCheck,
); );
} }
@ -2359,53 +2424,22 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
} }
}; };
Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass { Inbound.ShadowsocksSettings.Shadowsocks = class extends Inbound.ClientBase {
constructor( constructor(
method = '', method = '',
password = RandomUtil.randomShadowsocksPassword(), password = RandomUtil.randomShadowsocksPassword(),
email = RandomUtil.randomLowerAndNum(8), email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
limitIp = 0,
totalGB = 0,
expiryTime = 0,
enable = true,
tgId = '',
subId = RandomUtil.randomLowerAndNum(16),
comment = '',
reset = 0,
created_at = undefined,
updated_at = undefined
) { ) {
super(); super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.method = method; this.method = method;
this.password = password; this.password = password;
this.email = email;
this.limitIp = limitIp;
this.totalGB = totalGB;
this.expiryTime = expiryTime;
this.enable = enable;
this.tgId = tgId;
this.subId = subId;
this.comment = comment;
this.reset = reset;
this.created_at = created_at;
this.updated_at = updated_at;
} }
toJson() { toJson() {
return { return {
method: this.method, method: this.method,
password: this.password, password: this.password,
email: this.email, ...this._clientBaseToJson(),
limitIp: this.limitIp,
totalGB: this.totalGB,
expiryTime: this.expiryTime,
enable: this.enable,
tgId: this.tgId,
subId: this.subId,
comment: this.comment,
reset: this.reset,
created_at: this.created_at,
updated_at: this.updated_at,
}; };
} }
@ -2413,45 +2447,56 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
return new Inbound.ShadowsocksSettings.Shadowsocks( return new Inbound.ShadowsocksSettings.Shadowsocks(
json.method, json.method,
json.password, json.password,
json.email, ...Inbound.ClientBase.commonArgsFromJson(json),
json.limitIp, );
json.totalGB, }
json.expiryTime, };
json.enable,
json.tgId, Inbound.HysteriaSettings = class extends Inbound.Settings {
json.subId, constructor(protocol, version = 2, hysterias = [new Inbound.HysteriaSettings.Hysteria()]) {
json.comment, super(protocol);
json.reset, this.version = version;
json.created_at, this.hysterias = hysterias;
json.updated_at, }
static fromJson(json = {}) {
return new Inbound.HysteriaSettings(
Protocols.HYSTERIA,
json.version ?? 2,
(json.clients || []).map(client => Inbound.HysteriaSettings.Hysteria.fromJson(client)),
); );
} }
get _expiryTime() { toJson() {
if (this.expiryTime === 0 || this.expiryTime === "") { return {
return null; version: this.version,
} clients: Inbound.HysteriaSettings.toJsonArray(this.hysterias),
if (this.expiryTime < 0) { };
return this.expiryTime / -86400000; }
} };
return moment(this.expiryTime);
Inbound.HysteriaSettings.Hysteria = class extends Inbound.ClientBase {
constructor(
auth = RandomUtil.randomSeq(10),
email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at,
) {
super(email, limitIp, totalGB, expiryTime, enable, tgId, subId, comment, reset, created_at, updated_at);
this.auth = auth;
} }
set _expiryTime(t) { toJson() {
if (t == null || t === "") { return {
this.expiryTime = 0; auth: this.auth,
} else { ...this._clientBaseToJson(),
this.expiryTime = t.valueOf(); };
}
}
get _totalGB() {
return NumberFormatter.toFixed(this.totalGB / SizeFormatter.ONE_GB, 2);
} }
set _totalGB(gb) { static fromJson(json = {}) {
this.totalGB = NumberFormatter.toFixed(gb * SizeFormatter.ONE_GB, 0); return new Inbound.HysteriaSettings.Hysteria(
json.auth,
...Inbound.ClientBase.commonArgsFromJson(json),
);
} }
}; };
Inbound.TunnelSettings = class extends Inbound.Settings { Inbound.TunnelSettings = class extends Inbound.Settings {
@ -2707,4 +2752,4 @@ Inbound.TunSettings = class extends Inbound.Settings {
userLevel: this.userLevel || 0, userLevel: this.userLevel || 0,
}; };
} }
}; };

View file

@ -782,8 +782,8 @@ class Outbound extends CommonClass {
} }
canEnableTls() { canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.Hysteria].includes(this.protocol)) return false; if (this.protocol === Protocols.Hysteria) return true;
if (this.protocol === Protocols.Hysteria) return this.stream.network === 'hysteria'; if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network); return ["tcp", "ws", "http", "grpc", "httpupgrade", "xhttp"].includes(this.stream.network);
} }

View file

@ -30,7 +30,7 @@
</a-tooltip> </a-tooltip>
</template> </template>
<template slot="enable" slot-scope="text, client, index"> <template slot="enable" slot-scope="text, client, index">
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch> <a-switch v-model="client.enable" @change="switchEnableClient(record.id, client, $event)"></a-switch>
</template> </template>
<template slot="online" slot-scope="text, client, index"> <template slot="online" slot-scope="text, client, index">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover :overlay-class-name="themeSwitcher.currentTheme">
@ -165,7 +165,7 @@
<span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span> <span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item> <a-menu-item>
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id,client)"></a-switch> <a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id, client, $event)"></a-switch>
{{ i18n "enable"}} {{ i18n "enable"}}
</a-menu-item> </a-menu-item>
</a-menu> </a-menu>

View file

@ -1,5 +1,6 @@
{{define "form/client"}} {{define "form/client"}}
<a-form layout="horizontal" v-if="client" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> <a-form layout="horizontal" v-if="client" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'> <a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch> <a-switch v-model="client.enable"></a-switch>
</a-form-item> </a-form-item>
@ -10,38 +11,62 @@
<span>{{ i18n "pages.inbounds.emailDesc" }}</span> <span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template> </template>
{{ i18n "pages.inbounds.email" }} {{ i18n "pages.inbounds.email" }}
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon> <a-icon type="sync"
@click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.email"></a-input> <a-input v-model.trim="client.email"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS"> <a-form-item
v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
{{ i18n "password" }} {{ i18n "password" }}
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS" @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon> <a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"type="sync"> </a-icon> @click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
type="sync"></a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN"
@click="client.password = RandomUtil.randomSeq(10)"
type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.password"></a-input> <a-input v-model.trim="client.password"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"> <a-form-item v-if="inbound.protocol === Protocols.HYSTERIA">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
ID <a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon> Auth Password
<a-icon @click="client.auth = RandomUtil.randomSeq(10)"
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="client.auth"></a-input>
</a-form-item>
<a-form-item
v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
ID <a-icon @click="client.id = RandomUtil.randomUUID()"
type="sync"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.id"></a-input> <a-input v-model.trim="client.id"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'> <a-form-item v-if="inbound.protocol === Protocols.VMESS"
<a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme"> label='{{ i18n "security" }}'>
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option> <a-select v-model="client.security"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email && app.subSettings?.enable"> <a-form-item v-if="client.email && app.subSettings?.enable">
@ -51,7 +76,8 @@
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> <span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template> </template>
Subscription Subscription
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon> <a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)"
type="sync"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="client.subId"></a-input> <a-input v-model.trim="client.subId"></a-input>
@ -66,7 +92,8 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number> <a-input-number :style="{ width: '50%' }" v-model.number="client.tgId"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'> <a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
<a-input v-model.trim="client.comment"></a-input> <a-input v-model.trim="client.comment"></a-input>
@ -77,19 +104,21 @@
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitDesc"}}</span> <span>{{ i18n "pages.inbounds.IPLimitDesc"}}</span>
</template> </template>
<span>{{ i18n "pages.inbounds.IPLimit"}} </span> <span>{{ i18n "pages.inbounds.IPLimit"}} </span>
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="client.limitIp" min="0"></a-input-number> <a-input-number v-model.number="client.limitIp"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit"> <a-form-item
v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span> <span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
</template> </template>
<span>{{ i18n "pages.inbounds.IPLimitlog" }} </span> <span>{{ i18n "pages.inbounds.IPLimitlog" }} </span>
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
@ -98,19 +127,24 @@
<span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span> <span>{{ i18n "pages.inbounds.IPLimitlogclear" }}</span>
</template> </template>
<span :style="{ color: '#FF4D4F' }"> <span :style="{ color: '#FF4D4F' }">
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon> <a-icon type="delete"
@click="clearDBClientIps(client.email)"></a-icon>
</span> </span>
</a-tooltip> </a-tooltip>
<a-form layout="block"> <a-form layout="block">
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email)" placeholder="Click To Get IPs" <a-textarea id="clientIPs" readonly
@click="getDBClientIps(client.email)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 5, maxRows: 10 }"> :auto-size="{ minRows: 5, maxRows: 10 }">
</a-textarea> </a-textarea>
</a-form> </a-form>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.canEnableTlsFlow()" label='Flow'> <a-form-item v-if="inbound.canEnableTlsFlow()" label='Flow'>
<a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="client.flow"
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option value selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key
]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@ -123,45 +157,57 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="client._totalGB" :min="0"></a-input-number> <a-input-number v-model.number="client._totalGB"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'> <a-form-item v-if="isEdit && clientStats" label='{{ i18n "usage" }}'>
<a-tag :color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)"> <a-tag
:color="ColorUtils.clientUsageColor(clientStats, app.trafficDiff)">
[[ SizeFormatter.sizeFormat(clientStats.up) ]] / [[ SizeFormatter.sizeFormat(clientStats.up) ]] /
[[ SizeFormatter.sizeFormat(clientStats.down) ]] [[ SizeFormatter.sizeFormat(clientStats.down) ]]
([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]]) ([[ SizeFormatter.sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag> </a-tag>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic"
}}</template>
<a-icon type="retweet" <a-icon type="retweet"
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon> v-if="client.email.length > 0"></a-icon>
</a-tooltip> </a-tooltip>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch> <a-switch v-model="delayedStart"
@click="client._expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'> <a-form-item v-if="delayedStart"
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number v-model.number="delayedExpireDays"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template> <template slot="title">{{ i18n
"pages.inbounds.leaveBlankToNeverExpire" }}</template>
{{ i18n "pages.inbounds.expireDate" }} {{ i18n "pages.inbounds.expireDate" }}
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker v-if="datepicker == 'gregorian'"
:dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker> :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}' :dropdown-class-name="themeSwitcher.currentTheme"
value="client._expiryTime" v-model="client._expiryTime"></a-persian-datepicker> v-model="client._expiryTime"></a-date-picker>
<a-persian-datepicker v-else
placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="client._expiryTime"
v-model="client._expiryTime"></a-persian-datepicker>
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag> <a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
</a-form-item> </a-form-item>
<a-form-item v-if="client.expiryTime != 0"> <a-form-item v-if="client.expiryTime != 0">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.client.renewDesc" }}</template> <template slot="title">{{ i18n "pages.client.renewDesc"
}}</template>
{{ i18n "pages.client.renew" }} {{ i18n "pages.client.renew" }}
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>

View file

@ -74,7 +74,8 @@
<a-select-option value="never">{{ i18n <a-select-option value="never">{{ i18n
"pages.inbounds.periodicTrafficReset.never" }}</a-select-option> "pages.inbounds.periodicTrafficReset.never" }}</a-select-option>
<a-select-option value="hourly">{{ i18n <a-select-option value="hourly">{{ i18n
"pages.inbounds.periodicTrafficReset.hourly" }}</a-select-option> "pages.inbounds.periodicTrafficReset.hourly"
}}</a-select-option>
<a-select-option value="daily">{{ i18n <a-select-option value="daily">{{ i18n
"pages.inbounds.periodicTrafficReset.daily" }}</a-select-option> "pages.inbounds.periodicTrafficReset.daily" }}</a-select-option>
<a-select-option value="weekly">{{ i18n <a-select-option value="weekly">{{ i18n
@ -154,6 +155,11 @@
{{template "form/tun"}} {{template "form/tun"}}
</template> </template>
<!-- hysteria -->
<template v-if="inbound.protocol === Protocols.HYSTERIA">
{{template "form/hysteria"}}
</template>
<!-- stream settings --> <!-- stream settings -->
<template v-if="inbound.canEnableStream()"> <template v-if="inbound.canEnableStream()">
{{template "form/streamSettings"}} {{template "form/streamSettings"}}

View file

@ -0,0 +1,32 @@
{{define "form/hysteria"}}
<a-collapse activeKey="0"
v-for="(client, index) in inbound.settings.hysterias.slice(0,1)"
v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' +
inbound.settings.hysterias.length">
<table width="100%">
<tr class="client-table-header">
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Auth</th>
</tr>
<tr v-for="(client, index) in inbound.settings.hysterias"
:class="index % 2 == 1 ? 'client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.auth ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item :label="'{{ i18n "pages.inbounds.stream.tcp.version" }}'">
<a-input-number v-model.number="inbound.settings.version" :min="2"
:max="2" disabled></a-input-number>
</a-form-item>
</a-form>
{{end}}

View file

@ -0,0 +1,75 @@
{{define "form/streamHysteria"}}
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label='Auth Password'>
<a-input v-model.trim="inbound.stream.hysteria.auth"></a-input>
</a-form-item>
<a-form-item label='UDP Idle Timeout'>
<a-input-number v-model.number="inbound.stream.hysteria.udpIdleTimeout"
:min="0"></a-input-number>
</a-form-item>
<a-form-item label='Masquerade'>
<a-switch v-model="inbound.stream.hysteria.masqueradeSwitch"></a-switch>
</a-form-item>
<template v-if="inbound.stream.hysteria.masqueradeSwitch">
<a-divider :style="{ margin: '5px 0 0' }">Masquerade</a-divider>
<a-form-item label='Type'>
<a-select v-model="inbound.stream.hysteria.masquerade.type"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="file">File</a-select-option>
<a-select-option value="proxy">Proxy</a-select-option>
<a-select-option value="string">String</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Dir'
v-if="inbound.stream.hysteria.masquerade.type === 'file'">
<a-input
v-model.trim="inbound.stream.hysteria.masquerade.dir"></a-input>
</a-form-item>
<template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'">
<a-form-item label='URL'>
<a-input
v-model.trim="inbound.stream.hysteria.masquerade.url"></a-input>
</a-form-item>
<a-form-item label='Rewrite Host'>
<a-switch
v-model="inbound.stream.hysteria.masquerade.rewriteHost"></a-switch>
</a-form-item>
<a-form-item label='Insecure'>
<a-switch
v-model="inbound.stream.hysteria.masquerade.insecure"></a-switch>
</a-form-item>
</template>
<template v-if="inbound.stream.hysteria.masquerade.type === 'string'">
<a-form-item label='Content'>
<a-input
v-model.trim="inbound.stream.hysteria.masquerade.content"></a-input>
</a-form-item>
<a-form-item
label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small"
@click="inbound.stream.hysteria.masquerade.addHeader('', '')">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact
v-for="(header, index) in inbound.stream.hysteria.masquerade.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" style="margin: 0;">[[
index+1 ]]</template>
</a-input>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small"
@click="inbound.stream.hysteria.masquerade.removeHeader(index)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label='Status Code'>
<a-input-number
v-model.number="inbound.stream.hysteria.masquerade.statusCode"></a-input-number>
</a-form-item>
</template>
</template>
</a-form>
{{end}}

View file

@ -1,7 +1,8 @@
{{define "form/streamSettings"}} {{define "form/streamSettings"}}
<!-- select stream network --> <!-- select stream network -->
<a-form :colon="false" :label-col="{ md: {span:8} }" <a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"> :wrapper-col="{ md: {span:14} }"
v-if="inbound.protocol != Protocols.HYSTERIA">
<a-form-item label='{{ i18n "transmission" }}'> <a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" :style="{ width: '75%' }" <a-select v-model="inbound.stream.network" :style="{ width: '75%' }"
@change="streamNetworkChange" @change="streamNetworkChange"
@ -36,6 +37,11 @@
{{template "form/streamGRPC"}} {{template "form/streamGRPC"}}
</template> </template>
<!-- hysteria -->
<template v-if="inbound.stream.network === 'hysteria'">
{{template "form/streamHysteria"}}
</template>
<!-- httpupgrade --> <!-- httpupgrade -->
<template v-if="inbound.stream.network === 'httpupgrade'"> <template v-if="inbound.stream.network === 'httpupgrade'">
{{template "form/streamHTTPUpgrade"}} {{template "form/streamHTTPUpgrade"}}

View file

@ -2,15 +2,18 @@
<!-- tls enable --> <!-- tls enable -->
<a-form v-if="inbound.canEnableTls()" :colon="false" <a-form v-if="inbound.canEnableTls()" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '3px 0' }"></a-divider> <template v-if="inbound.protocol !== Protocols.HYSTERIA">
<a-form-item label='{{ i18n "security" }}'> <a-divider :style="{ margin: '3px 0' }"></a-divider>
<a-radio-group v-model="inbound.stream.security" button-style="solid"> <a-form-item label='{{ i18n "security" }}'>
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button> <a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button v-if="inbound.canEnableReality()" <a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
value="reality">Reality</a-radio-button> <a-radio-button v-if="inbound.canEnableReality()"
<a-radio-button value="tls">TLS</a-radio-button> value="reality">Reality</a-radio-button>
</a-radio-group> <a-radio-button value="tls">TLS</a-radio-button>
</a-form-item> </a-radio-group>
</a-form-item>
</template>
<template v-else><a-divider style="margin:0;">TLS</a-divider></template>
<!-- tls settings --> <!-- tls settings -->
<template v-if="inbound.stream.isTls"> <template v-if="inbound.stream.isTls">

View file

@ -2,14 +2,18 @@
{{ template "page/head_end" .}} {{ template "page/head_end" .}}
{{ template "page/body_start" .}} {{ template "page/body_start" .}}
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' inbounds-page'"> <a-layout id="app" v-cloak
:class="themeSwitcher.currentTheme + ' inbounds-page'">
<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"}}' size="large"> <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"
message='{{ i18n "secAlertTitle" }}' color="red" description='{{ i18n "secAlertSsl" }}' show-icon closable> :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}' color="red"
description='{{ i18n "secAlertSsl" }}' show-icon closable>
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
@ -21,7 +25,8 @@
<a-card size="small" :style="{ padding: '16px' }" hoverable> <a-card size="small" :style="{ padding: '16px' }" hoverable>
<a-row> <a-row>
<a-col :sm="12" :md="5"> <a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' <a-custom-statistic
title='{{ i18n "pages.inbounds.totalDownUp" }}'
:value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`"> :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
<template #prefix> <template #prefix>
<a-icon type="swap"></a-icon> <a-icon type="swap"></a-icon>
@ -29,7 +34,8 @@
</a-custom-statistic> </a-custom-statistic>
</a-col> </a-col>
<a-col :sm="12" :md="5"> <a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' <a-custom-statistic
title='{{ i18n "pages.inbounds.totalUsage" }}'
:value="SizeFormatter.sizeFormat(total.up + total.down)" :value="SizeFormatter.sizeFormat(total.up + total.down)"
:style="{ marginTop: isMobile ? '10px' : 0 }"> :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix> <template #prefix>
@ -38,15 +44,19 @@
</a-custom-statistic> </a-custom-statistic>
</a-col> </a-col>
<a-col :sm="12" :md="5"> <a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}' <a-custom-statistic
:value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }"> title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}'
:value="SizeFormatter.sizeFormat(total.allTime)"
:style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix> <template #prefix>
<a-icon type="history"></a-icon> <a-icon type="history"></a-icon>
</template> </template>
</a-custom-statistic> </a-custom-statistic>
</a-col> </a-col>
<a-col :sm="12" :md="5"> <a-col :sm="12" :md="5">
<a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" <a-custom-statistic
title='{{ i18n "pages.inbounds.inboundCount" }}'
:value="dbInbounds.length"
:style="{ marginTop: isMobile ? '10px' : 0 }"> :style="{ marginTop: isMobile ? '10px' : 0 }">
<template #prefix> <template #prefix>
<a-icon type="bars"></a-icon> <a-icon type="bars"></a-icon>
@ -60,33 +70,50 @@
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-icon type="team"></a-icon> <a-icon type="team"></a-icon>
<div> <div>
<a-back-top :target="() => document.getElementById('content-layout')" <a-back-top
:target="() => document.getElementById('content-layout')"
visibility-height="200"></a-back-top> visibility-height="200"></a-back-top>
<a-tag color="green">[[ total.clients ]]</a-tag> <a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "disabled" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in total.deactive"><span>[[ clientEmail ]]</span></div> <div
v-for="clientEmail in total.deactive"><span>[[
clientEmail ]]</span></div>
</template> </template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag> <a-tag v-if="total.deactive.length">[[
total.deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "depleted" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in total.depleted"><span>[[ clientEmail ]]</span></div> <div
v-for="clientEmail in total.depleted"><span>[[
clientEmail ]]</span></div>
</template> </template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag> <a-tag color="red" v-if="total.depleted.length">[[
total.depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' <a-popover title='{{ i18n "depletingSoon" }}'
:overlay-class-name="themeSwitcher.currentTheme"> :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in total.expiring"><span>[[ clientEmail ]]</span></div> <div
v-for="clientEmail in total.expiring"><span>[[
clientEmail ]]</span></div>
</template> </template>
<a-tag color="orange" v-if="total.expiring.length">[[ total.expiring.length ]]</a-tag> <a-tag color="orange"
v-if="total.expiring.length">[[
total.expiring.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "online" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in onlineClients"><span>[[ clientEmail ]]</span></div> <div
v-for="clientEmail in onlineClients"><span>[[
clientEmail ]]</span></div>
</template> </template>
<a-tag color="blue" v-if="onlineClients.length">[[ onlineClients.length ]]</a-tag> <a-tag color="blue" v-if="onlineClients.length">[[
onlineClients.length ]]</a-tag>
</a-popover> </a-popover>
</div> </div>
</a-space> </a-space>
@ -100,14 +127,18 @@
<a-card hoverable> <a-card hoverable>
<template #title> <template #title>
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-button type="primary" icon="plus" @click="openAddInbound"> <a-button type="primary" icon="plus"
<template v-if="!isMobile">{{ i18n "pages.inbounds.addInbound" }}</template> @click="openAddInbound">
<template v-if="!isMobile">{{ i18n
"pages.inbounds.addInbound" }}</template>
</a-button> </a-button>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu"> <a-button type="primary" icon="menu">
<template v-if="!isMobile">{{ i18n "pages.inbounds.generalActions" }}</template> <template v-if="!isMobile">{{ i18n
"pages.inbounds.generalActions" }}</template>
</a-button> </a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="themeSwitcher.currentTheme"> <a-menu slot="overlay" @click="a => generalActions(a)"
:theme="themeSwitcher.currentTheme">
<a-menu-item key="import"> <a-menu-item key="import">
<a-icon type="import"></a-icon> <a-icon type="import"></a-icon>
{{ i18n "pages.inbounds.importInbound" }} {{ i18n "pages.inbounds.importInbound" }}
@ -118,7 +149,8 @@
</a-menu-item> </a-menu-item>
<a-menu-item key="subs" v-if="subSettings.enable"> <a-menu-item key="subs" v-if="subSettings.enable">
<a-icon type="export"></a-icon> <a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }} {{ i18n "pages.inbounds.export" }} - {{ i18n
"pages.settings.subSettings" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="resetInbounds"> <a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon> <a-icon type="reload"></a-icon>
@ -128,7 +160,8 @@
<a-icon type="file-done"></a-icon> <a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }} {{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }"> <a-menu-item key="delDepletedClients"
:style="{ color: '#FF4D4F' }">
<a-icon type="rest"></a-icon> <a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }} {{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item> </a-menu-item>
@ -138,20 +171,28 @@
</template> </template>
<template #extra> <template #extra>
<a-button-group> <a-button-group>
<a-button icon="sync" @click="manualRefresh" :loading="refreshing"></a-button> <a-button icon="sync" @click="manualRefresh"
<a-popover placement="bottomRight" trigger="click" :overlay-class-name="themeSwitcher.currentTheme"> :loading="refreshing"></a-button>
<a-popover placement="bottomRight" trigger="click"
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
<div class="ant-custom-popover-title"> <div class="ant-custom-popover-title">
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh" size="small"></a-switch> <a-switch v-model="isRefreshEnabled"
@change="toggleRefresh" size="small"></a-switch>
<span>{{ i18n "pages.inbounds.autoRefresh" }}</span> <span>{{ i18n "pages.inbounds.autoRefresh" }}</span>
</div> </div>
</template> </template>
<template #content> <template #content>
<a-space direction="vertical"> <a-space direction="vertical">
<span>{{ i18n "pages.inbounds.autoRefreshInterval" }}</span> <span>{{ i18n "pages.inbounds.autoRefreshInterval"
<a-select v-model="refreshInterval" :disabled="!isRefreshEnabled" :style="{ width: '100%' }" }}</span>
@change="changeRefreshInterval" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="refreshInterval"
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option> :disabled="!isRefreshEnabled"
:style="{ width: '100%' }"
@change="changeRefreshInterval"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in [5,10,30,60]"
:value="key*1000">[[ key ]]s</a-select-option>
</a-select> </a-select>
</a-space> </a-space>
</template> </template>
@ -160,27 +201,38 @@
</a-button-group> </a-button-group>
</template> </template>
<a-space direction="vertical"> <a-space direction="vertical">
<div :style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }"> <div
:style="isMobile ? {} : { display: 'flex', alignItems: 'center', justifyContent: 'flex-start' }">
<a-switch v-model="enableFilter" <a-switch v-model="enableFilter"
:style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }" :style="isMobile ? { marginBottom: '.5rem', display: 'flex' } : { marginRight: '.5rem' }"
@change="toggleFilter"> @change="toggleFilter">
<a-icon slot="checkedChildren" type="search"></a-icon> <a-icon slot="checkedChildren" type="search"></a-icon>
<a-icon slot="unCheckedChildren" type="filter"></a-icon> <a-icon slot="unCheckedChildren" type="filter"></a-icon>
</a-switch> </a-switch>
<a-input v-if="!enableFilter" v-model.lazy="searchKey" placeholder='{{ i18n "search" }}' autofocus <a-input v-if="!enableFilter" v-model.lazy="searchKey"
:style="{ maxWidth: '300px' }" :size="isMobile ? 'small' : ''"></a-input> placeholder='{{ i18n "search" }}' autofocus
<a-radio-group v-if="enableFilter" v-model="filterBy" @change="filterInbounds" button-style="solid" :style="{ maxWidth: '300px' }"
:size="isMobile ? 'small' : ''"></a-input>
<a-radio-group v-if="enableFilter" v-model="filterBy"
@change="filterInbounds" button-style="solid"
:size="isMobile ? 'small' : ''"> :size="isMobile ? 'small' : ''">
<a-radio-button value="">{{ i18n "none" }}</a-radio-button> <a-radio-button value>{{ i18n "none" }}</a-radio-button>
<a-radio-button value="deactive">{{ i18n "disabled" }}</a-radio-button> <a-radio-button value="deactive">{{ i18n "disabled"
<a-radio-button value="depleted">{{ i18n "depleted" }}</a-radio-button> }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon" }}</a-radio-button> <a-radio-button value="depleted">{{ i18n "depleted"
<a-radio-button value="online">{{ i18n "online" }}</a-radio-button> }}</a-radio-button>
<a-radio-button value="expiring">{{ i18n "depletingSoon"
}}</a-radio-button>
<a-radio-button value="online">{{ i18n "online"
}}</a-radio-button>
</a-radio-group> </a-radio-group>
</div> </div>
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="isMobile ? mobileColumns : columns"
:data-source="searchedInbounds" :scroll="isMobile ? {} : { x: 1000 }" :row-key="dbInbound => dbInbound.id"
:pagination=pagination(searchedInbounds) :expand-icon-as-cell="false" :expand-row-by-click="false" :data-source="searchedInbounds"
:scroll="isMobile ? {} : { x: 1000 }"
:pagination=pagination(searchedInbounds)
:expand-icon-as-cell="false" :expand-row-by-click="false"
:expand-icon-column-index="0" :indent-size="0" :expand-icon-column-index="0" :indent-size="0"
:row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')" :row-class-name="dbInbound => (dbInbound.isMultiUser() ? '' : 'hideExpandIcon')"
:style="{ marginTop: '10px' }" :style="{ marginTop: '10px' }"
@ -189,7 +241,8 @@
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" <a-icon @click="e => e.preventDefault()" type="more"
:style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon> :style="{ fontSize: '20px', textDecoration: 'solid' }"></a-icon>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" <a-menu slot="overlay"
@click="a => clickAction(a, dbInbound)"
:theme="themeSwitcher.currentTheme"> :theme="themeSwitcher.currentTheme">
<a-menu-item key="edit"> <a-menu-item key="edit">
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
@ -211,7 +264,8 @@
</a-menu-item> </a-menu-item>
<a-menu-item key="resetClients"> <a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon> <a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetInboundClientTraffics"}} {{ i18n
"pages.inbounds.resetInboundClientTraffics"}}
</a-menu-item> </a-menu-item>
<a-menu-item key="export"> <a-menu-item key="export">
<a-icon type="export"></a-icon> <a-icon type="export"></a-icon>
@ -219,9 +273,11 @@
</a-menu-item> </a-menu-item>
<a-menu-item key="subs" v-if="subSettings.enable"> <a-menu-item key="subs" v-if="subSettings.enable">
<a-icon type="export"></a-icon> <a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }} {{ i18n "pages.inbounds.export"}} - {{ i18n
"pages.settings.subSettings" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="delDepletedClients" :style="{ color: '#FF4D4F' }"> <a-menu-item key="delDepletedClients"
:style="{ color: '#FF4D4F' }">
<a-icon type="rest"></a-icon> <a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }} {{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item> </a-menu-item>
@ -237,10 +293,12 @@
{{ i18n "pages.inbounds.exportInbound" }} {{ i18n "pages.inbounds.exportInbound" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="resetTraffic"> <a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }} <a-icon type="retweet"></a-icon> {{ i18n
"pages.inbounds.resetTraffic" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="clone"> <a-menu-item key="clone">
<a-icon type="block"></a-icon> {{ i18n "pages.inbounds.clone"}} <a-icon type="block"></a-icon> {{ i18n
"pages.inbounds.clone"}}
</a-menu-item> </a-menu-item>
<a-menu-item key="delete"> <a-menu-item key="delete">
<span :style="{ color: '#FF4D4F' }"> <span :style="{ color: '#FF4D4F' }">
@ -256,26 +314,38 @@
</a-dropdown> </a-dropdown>
</template> </template>
<template slot="protocol" slot-scope="text, dbInbound"> <template slot="protocol" slot-scope="text, dbInbound">
<a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag> <a-tag :style="{ margin: '0' }" color="purple">[[
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> dbInbound.protocol ]]</a-tag>
<a-tag :style="{ margin: '0' }" color="green">[[ dbInbound.toInbound().stream.network ]]</a-tag> <template
<a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag :style="{ margin: '0' }" color="green">[[
dbInbound.toInbound().stream.network ]]</a-tag>
<a-tag :style="{ margin: '0' }"
v-if="dbInbound.toInbound().stream.isTls"
color="blue">TLS</a-tag> color="blue">TLS</a-tag>
<a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" <a-tag :style="{ margin: '0' }"
v-if="dbInbound.toInbound().stream.isReality"
color="blue">Reality</a-tag> color="blue">Reality</a-tag>
</template> </template>
</template> </template>
<template slot="clients" slot-scope="text, dbInbound"> <template slot="clients" slot-scope="text, dbInbound">
<template v-if="clientCount[dbInbound.id]"> <template v-if="clientCount[dbInbound.id]">
<a-tag :style="{ margin: '0' }" color="green">[[ clientCount[dbInbound.id].clients ]]</a-tag> <a-tag :style="{ margin: '0' }" color="green">[[
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="themeSwitcher.currentTheme"> clientCount[dbInbound.id].clients ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].deactive"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
@ -286,84 +356,113 @@
v-if="clientCount[dbInbound.id].deactive.length">[[ v-if="clientCount[dbInbound.id].deactive.length">[[
clientCount[dbInbound.id].deactive.length ]]</a-tag> clientCount[dbInbound.id].deactive.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "depleted" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].depleted"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" <a-tag :style="{ margin: '0', padding: '0 2px' }"
color="red"
v-if="clientCount[dbInbound.id].depleted.length">[[ v-if="clientCount[dbInbound.id].depleted.length">[[
clientCount[dbInbound.id].depleted.length ]]</a-tag> clientCount[dbInbound.id].depleted.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "depletingSoon" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].expiring"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" <a-tag :style="{ margin: '0', padding: '0 2px' }"
color="orange"
v-if="clientCount[dbInbound.id].expiring.length">[[ v-if="clientCount[dbInbound.id].expiring.length">[[
clientCount[dbInbound.id].expiring.length ]]</a-tag> clientCount[dbInbound.id].expiring.length ]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "online" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].online"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" color="blue" <a-tag :style="{ margin: '0', padding: '0 2px' }"
v-if="clientCount[dbInbound.id].online.length">[[ clientCount[dbInbound.id].online.length color="blue"
v-if="clientCount[dbInbound.id].online.length">[[
clientCount[dbInbound.id].online.length
]]</a-tag> ]]</a-tag>
</a-popover> </a-popover>
</template> </template>
</template> </template>
<template slot="traffic" slot-scope="text, dbInbound"> <template slot="traffic" slot-scope="text, dbInbound">
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<table cellpadding="2" width="100%"> <table cellpadding="2" width="100%">
<tr> <tr>
<td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td> <td>↑[[ SizeFormatter.sizeFormat(dbInbound.up)
<td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td> ]]</td>
<td>↓[[ SizeFormatter.sizeFormat(dbInbound.down)
]]</td>
</tr> </tr>
<tr v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total"> <tr
v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td> <td>{{ i18n "remained" }}</td>
<td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) ]]</td> <td>[[ SizeFormatter.sizeFormat(dbInbound.total -
dbInbound.up - dbInbound.down) ]]</td>
</tr> </tr>
</table> </table>
</template> </template>
<a-tag <a-tag
:color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)"> :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] / [[ SizeFormatter.sizeFormat(dbInbound.up +
dbInbound.down) ]] /
<template v-if="dbInbound.total > 0"> <template v-if="dbInbound.total > 0">
[[ SizeFormatter.sizeFormat(dbInbound.total) ]] [[ SizeFormatter.sizeFormat(dbInbound.total) ]]
</template> </template>
<template v-else> <template v-else>
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px"
viewBox="0 0 640 512" fill="currentColor">
<path <path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path> fill="currentColor"></path>
@ -372,25 +471,30 @@
</a-tag> </a-tag>
</a-popover> </a-popover>
</template> </template>
<template slot="allTimeInbound" slot-scope="text, dbInbound"> <template slot="allTimeInbound"
<a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag> slot-scope="text, dbInbound">
<a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0)
]]</a-tag>
</template> </template>
<template slot="enable" slot-scope="text, dbInbound"> <template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" <a-switch v-model="dbInbound.enable"
@change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch> @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
</template> </template>
<template slot="expiryTime" slot-scope="text, dbInbound"> <template slot="expiryTime" slot-scope="text, dbInbound">
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme"> <a-popover v-if="dbInbound.expiryTime > 0"
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
[[ IntlUtil.formatDate(dbInbound.expiryTime) ]] [[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
</template> </template>
<a-tag :style="{ minWidth: '50px' }" <a-tag :style="{ minWidth: '50px' }"
:color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)"> :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
[[ IntlUtil.formatRelativeTime(dbInbound.expiryTime) ]] [[ IntlUtil.formatRelativeTime(dbInbound.expiryTime)
]]
</a-tag> </a-tag>
</a-popover> </a-popover>
<a-tag v-else color="purple" class="infinite-tag"> <a-tag v-else color="purple" class="infinite-tag">
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px" viewBox="0 0 640 512"
fill="currentColor">
<path <path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path> fill="currentColor"></path>
@ -398,21 +502,28 @@
</a-tag> </a-tag>
</template> </template>
<template slot="info" slot-scope="text, dbInbound"> <template slot="info" slot-scope="text, dbInbound">
<a-popover placement="bottomRight" :overlay-class-name="themeSwitcher.currentTheme" <a-popover placement="bottomRight"
:overlay-class-name="themeSwitcher.currentTheme"
trigger="click"> trigger="click">
<template slot="content"> <template slot="content">
<table cellpadding="2"> <table cellpadding="2">
<tr> <tr>
<td>{{ i18n "pages.inbounds.protocol" }}</td> <td>{{ i18n "pages.inbounds.protocol" }}</td>
<td> <td>
<a-tag :style="{ margin: '0' }" color="purple">[[ dbInbound.protocol ]]</a-tag> <a-tag :style="{ margin: '0' }"
color="purple">[[ dbInbound.protocol
]]</a-tag>
<template <template
v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS"> v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag :style="{ margin: '0' }" color="blue">[[ dbInbound.toInbound().stream.network <a-tag :style="{ margin: '0' }"
color="blue">[[
dbInbound.toInbound().stream.network
]]</a-tag> ]]</a-tag>
<a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isTls" <a-tag :style="{ margin: '0' }"
v-if="dbInbound.toInbound().stream.isTls"
color="green">tls</a-tag> color="green">tls</a-tag>
<a-tag :style="{ margin: '0' }" v-if="dbInbound.toInbound().stream.isReality" <a-tag :style="{ margin: '0' }"
v-if="dbInbound.toInbound().stream.isReality"
color="green">reality</a-tag> color="green">reality</a-tag>
</template> </template>
</td> </td>
@ -424,111 +535,156 @@
<tr v-if="clientCount[dbInbound.id]"> <tr v-if="clientCount[dbInbound.id]">
<td>{{ i18n "clients" }}</td> <td>{{ i18n "clients" }}</td>
<td> <td>
<a-tag :style="{ margin: '0' }" color="blue">[[ clientCount[dbInbound.id].clients <a-tag :style="{ margin: '0' }" color="blue">[[
clientCount[dbInbound.id].clients
]]</a-tag> ]]</a-tag>
<a-popover title='{{ i18n "disabled" }}' <a-popover title='{{ i18n "disabled" }}'
:overlay-class-name="themeSwitcher.currentTheme"> :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].deactive" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].deactive"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" <a-tag
:style="{ margin: '0', padding: '0 2px' }"
v-if="clientCount[dbInbound.id].deactive.length">[[ v-if="clientCount[dbInbound.id].deactive.length">[[
clientCount[dbInbound.id].deactive.length ]]</a-tag> clientCount[dbInbound.id].deactive.length
]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depleted" }}' <a-popover title='{{ i18n "depleted" }}'
:overlay-class-name="themeSwitcher.currentTheme"> :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].depleted" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].depleted"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" color="red" <a-tag
:style="{ margin: '0', padding: '0 2px' }"
color="red"
v-if="clientCount[dbInbound.id].depleted.length">[[ v-if="clientCount[dbInbound.id].depleted.length">[[
clientCount[dbInbound.id].depleted.length ]]</a-tag> clientCount[dbInbound.id].depleted.length
]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "depletingSoon" }}' <a-popover title='{{ i18n "depletingSoon" }}'
:overlay-class-name="themeSwitcher.currentTheme"> :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].expiring" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].expiring"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" color="orange" <a-tag
:style="{ margin: '0', padding: '0 2px' }"
color="orange"
v-if="clientCount[dbInbound.id].expiring.length">[[ v-if="clientCount[dbInbound.id].expiring.length">[[
clientCount[dbInbound.id].expiring.length ]]</a-tag> clientCount[dbInbound.id].expiring.length
]]</a-tag>
</a-popover> </a-popover>
<a-popover title='{{ i18n "online" }}' :overlay-class-name="themeSwitcher.currentTheme"> <a-popover title='{{ i18n "online" }}'
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<div v-for="clientEmail in clientCount[dbInbound.id].online" :key="clientEmail" <div
v-for="clientEmail in clientCount[dbInbound.id].online"
:key="clientEmail"
class="client-popup-item"> class="client-popup-item">
<span>[[ clientEmail ]]</span> <span>[[ clientEmail ]]</span>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"> <a-tooltip
:overlay-class-name="themeSwitcher.currentTheme">
<template #title> <template #title>
[[ clientCount[dbInbound.id].comments.get(clientEmail) ]] [[
clientCount[dbInbound.id].comments.get(clientEmail)
]]
</template> </template>
<a-icon type="message" <a-icon type="message"
v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon> v-if="clientCount[dbInbound.id].comments.get(clientEmail)"></a-icon>
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<a-tag :style="{ margin: '0', padding: '0 2px' }" color="green" <a-tag
:style="{ margin: '0', padding: '0 2px' }"
color="green"
v-if="clientCount[dbInbound.id].online.length">[[ v-if="clientCount[dbInbound.id].online.length">[[
clientCount[dbInbound.id].online.length ]]</a-tag> clientCount[dbInbound.id].online.length
]]</a-tag>
</a-popover> </a-popover>
</td> </td>
</tr> </tr>
<tr> <tr>
<td>{{ i18n "pages.inbounds.traffic" }}</td> <td>{{ i18n "pages.inbounds.traffic" }}</td>
<td> <td>
<a-popover :overlay-class-name="themeSwitcher.currentTheme"> <a-popover
:overlay-class-name="themeSwitcher.currentTheme">
<template slot="content"> <template slot="content">
<table cellpadding="2" width="100%"> <table cellpadding="2" width="100%">
<tr> <tr>
<td>↑[[ SizeFormatter.sizeFormat(dbInbound.up) ]]</td> <td>↑[[
<td>↓[[ SizeFormatter.sizeFormat(dbInbound.down) ]]</td> SizeFormatter.sizeFormat(dbInbound.up)
]]</td>
<td>↓[[
SizeFormatter.sizeFormat(dbInbound.down)
]]</td>
</tr> </tr>
<tr <tr
v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total"> v-if="dbInbound.total > 0 && dbInbound.up + dbInbound.down < dbInbound.total">
<td>{{ i18n "remained" }}</td> <td>{{ i18n "remained" }}</td>
<td>[[ SizeFormatter.sizeFormat(dbInbound.total - dbInbound.up - dbInbound.down) <td>[[
SizeFormatter.sizeFormat(dbInbound.total
- dbInbound.up - dbInbound.down)
]]</td> ]]</td>
</tr> </tr>
</table> </table>
</template> </template>
<a-tag <a-tag
:color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)"> :color="ColorUtils.usageColor(dbInbound.up + dbInbound.down, app.trafficDiff, dbInbound.total)">
[[ SizeFormatter.sizeFormat(dbInbound.up + dbInbound.down) ]] / [[ SizeFormatter.sizeFormat(dbInbound.up +
dbInbound.down) ]] /
<template v-if="dbInbound.total > 0"> <template v-if="dbInbound.total > 0">
[[ SizeFormatter.sizeFormat(dbInbound.total) ]] [[
SizeFormatter.sizeFormat(dbInbound.total)
]]
</template> </template>
<template v-else> <template v-else>
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> <svg height="10px" width="14px"
viewBox="0 0 640 512"
fill="currentColor">
<path <path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path> fill="currentColor"></path>
@ -541,12 +697,17 @@
<tr> <tr>
<td>{{ i18n "pages.inbounds.expireDate" }}</td> <td>{{ i18n "pages.inbounds.expireDate" }}</td>
<td> <td>
<a-tag :style="{ minWidth: '50px', textAlign: 'center' }" <a-tag
v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'"> :style="{ minWidth: '50px', textAlign: 'center' }"
[[ IntlUtil.formatDate(dbInbound.expiryTime) ]] v-if="dbInbound.expiryTime > 0"
:color="dbInbound.isExpiry? 'red': 'blue'">
[[ IntlUtil.formatDate(dbInbound.expiryTime)
]]
</a-tag> </a-tag>
<a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag"> <a-tag v-else :style="{ textAlign: 'center' }"
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor"> color="purple" class="infinite-tag">
<svg height="10px" width="14px"
viewBox="0 0 640 512" fill="currentColor">
<path <path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path> fill="currentColor"></path>
@ -555,25 +716,32 @@
</td> </td>
</tr> </tr>
<tr> <tr>
<td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td> <td>{{ i18n
"pages.inbounds.periodicTrafficResetTitle"
}}</td>
<td> <td>
<a-tag color="blue">[[ dbInbound.trafficReset ]]</a-tag> <a-tag color="blue">[[ dbInbound.trafficReset
]]</a-tag>
</td> </td>
</tr> </tr>
</table> </table>
</template> </template>
<a-badge> <a-badge>
<a-icon v-if="!dbInbound.enable" slot="count" type="pause-circle" <a-icon v-if="!dbInbound.enable" slot="count"
type="pause-circle"
:style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon> :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
<a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }"> <a-button shape="round" size="small"
:style="{ fontSize: '14px', padding: '0 10px' }">
<a-icon type="info"></a-icon> <a-icon type="info"></a-icon>
</a-button> </a-button>
</a-badge> </a-badge>
</a-popover> </a-popover>
</template> </template>
<template slot="expandedRowRender" slot-scope="record"> <template slot="expandedRowRender" slot-scope="record">
<a-table :row-key="client => client.id" :columns="isMobile ? innerMobileColumns : innerColumns" <a-table :row-key="client => client.id"
:data-source="getInboundClients(record)" :pagination=pagination(getInboundClients(record)) :columns="isMobile ? innerMobileColumns : innerColumns"
:data-source="getInboundClients(record)"
:pagination=pagination(getInboundClients(record))
:style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }"> :style="{ margin: `-10px ${isMobile ? '2px' : '22px'} -11px` }">
{{template "component/aClientTable"}} {{template "component/aClientTable"}}
</a-table> </a-table>
@ -589,11 +757,15 @@
</a-layout> </a-layout>
</a-layout> </a-layout>
{{template "page/body_scripts" .}} {{template "page/body_scripts" .}}
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script> <script
src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script> <script
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script> src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script> <script
src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
<script
src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
{{template "component/aSidebar" .}} {{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}} {{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}} {{template "component/aCustomStatistic" .}}
@ -1247,6 +1419,7 @@
switch (protocol) { switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email; case Protocols.SHADOWSOCKS: return client.email;
case Protocols.HYSTERIA: return client.auth;
default: return client.id; default: return client.id;
} }
}, },
@ -1322,19 +1495,25 @@
this.submit(`/panel/api/inbounds/update/${dbInboundId}`, formData); this.submit(`/panel/api/inbounds/update/${dbInboundId}`, formData);
}, },
async switchEnableClient(dbInboundId, client) { async switchEnableClient(dbInboundId, client, state) {
this.loading() this.loading();
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId); try {
if (!dbInbound) return; dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
inbound = dbInbound.toInbound(); if (!dbInbound) return;
clients = inbound && inbound.clients ? inbound.clients : null;
if (!clients || !Array.isArray(clients)) return; inbound = dbInbound.toInbound();
index = this.findIndexOfClient(dbInbound.protocol, clients, client); clients = inbound && inbound.clients ? inbound.clients : null;
if (index < 0 || !clients[index]) return; if (!clients || !Array.isArray(clients)) return;
clients[index].enable = !clients[index].enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]); index = this.findIndexOfClient(dbInbound.protocol, clients, client);
await this.updateClient(clients[index], dbInboundId, clientId); if (index < 0 || !clients[index]) return;
this.loading(false);
clients[index].enable = typeof state === 'boolean' ? state : !!client.enable;
clientId = this.getClientId(dbInbound.protocol, clients[index]);
await this.updateClient(clients[index], dbInboundId, clientId);
} finally {
this.loading(false);
}
}, },
async submit(url, data, modal) { async submit(url, data, modal) {
const msg = await HttpUtil.postWithModal(url, data, modal); const msg = await HttpUtil.postWithModal(url, data, modal);

View file

@ -1,52 +1,75 @@
{{define "modals/clientsBulkModal"}} {{define "modals/clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" <a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible"
@ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false" :title="clientsBulkModal.title"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme"> @ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading"
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"> :closable="true" :mask-closable="false"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.client.method" }}'> <a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" <a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid"
:dropdown-class-name="themeSwitcher.currentTheme"> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="0">Random</a-select-option> <a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option> <a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option> <a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option> <a-select-option
:value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option> <a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1"> <a-form-item label='{{ i18n "pages.client.first" }}'
<a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number> v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.firstNum"
:min="1"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1"> <a-form-item label='{{ i18n "pages.client.last" }}'
<a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number> v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.lastNum"
:min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0"> <a-form-item label='{{ i18n "pages.client.prefix" }}'
v-if="clientsBulkModal.emailMethod>0">
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input> <a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2"> <a-form-item label='{{ i18n "pages.client.postfix" }}'
v-if="clientsBulkModal.emailMethod>2">
<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" }}'
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="500"></a-input-number> v-if="clientsBulkModal.emailMethod < 2">
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1"
:max="500"></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" }}'
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme"> v-if="inbound.protocol === Protocols.VMESS">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option> <a-select v-model="clientsBulkModal.security"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[
key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()"> <a-form-item label='Flow'
<a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme"> v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select v-model="clientsBulkModal.flow"
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value selected>{{ i18n "none"
}}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="app.subSettings?.enable"> <a-form-item v-if="app.subSettings?.enable">
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span> <span>{{ i18n "pages.inbounds.subscriptionDesc"
}}</span>
</template> </template>
Subscription Subscription
<a-icon @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon> <a-icon
@click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)"
type="sync"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="clientsBulkModal.subId"></a-input> <a-input v-model.trim="clientsBulkModal.subId"></a-input>
@ -61,7 +84,8 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number :style="{ width: '50%' }" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number> <a-input-number :style="{ width: '50%' }"
v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-if="app.ipLimitEnable"> <a-form-item v-if="app.ipLimitEnable">
<template slot="label"> <template slot="label">
@ -73,7 +97,8 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number> <a-input-number v-model.number="clientsBulkModal.limitIp"
min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">
@ -85,29 +110,38 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number> <a-input-number v-model.number="clientsBulkModal.totalGB"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'> <a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch> <a-switch v-model="clientsBulkModal.delayedStart"
@click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart"> <a-form-item label='{{ i18n "pages.client.expireDays" }}'
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number> v-if="clientsBulkModal.delayedStart">
<a-input-number v-model.number="delayedExpireDays"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item v-else> <a-form-item v-else>
<template slot="label"> <template slot="label">
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span> <span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire"
}}</span>
</template> </template>
{{ i18n "pages.inbounds.expireDate" }} {{ i18n "pages.inbounds.expireDate" }}
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" <a-date-picker v-if="datepicker == 'gregorian'"
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="clientsBulkModal.expiryTime"></a-date-picker> v-model="clientsBulkModal.expiryTime"></a-date-picker>
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}' <a-persian-datepicker v-else
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime"> placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="clientsBulkModal.expiryTime"
v-model="clientsBulkModal.expiryTime">
</a-persian-datepicker> </a-persian-datepicker>
</a-form-item> </a-form-item>
<a-form-item v-if="clientsBulkModal.expiryTime != 0"> <a-form-item v-if="clientsBulkModal.expiryTime != 0">
@ -120,7 +154,8 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number> <a-input-number v-model.number="clientsBulkModal.reset"
:min="0"></a-input-number>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-modal> </a-modal>
@ -214,6 +249,7 @@
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS(); case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan(); case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method); case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
case Protocols.HYSTERIA: return new Inbound.HysteriaSettings.Hysteria();
default: return null; default: return null;
} }
}, },

View file

@ -1,10 +1,14 @@
{{define "modals/clientsModal"}} {{define "modals/clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok" <a-modal id="client-modal" v-model="clientModal.visible"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false" :title="clientModal.title" @ok="clientModal.ok"
:class="themeSwitcher.currentTheme" :confirm-loading="clientModal.confirmLoading" :closable="true"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'> :mask-closable="false"
:class="themeSwitcher.currentTheme"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
<template v-if="isEdit"> <template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" :style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account is (Expired|Traffic Ended) And Disabled</a-tag> <a-tag v-if="isExpiry || isTrafficExhausted" color="red"
:style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account
is (Expired|Traffic Ended) And Disabled</a-tag>
</template> </template>
{{template "form/client"}} {{template "form/client"}}
</a-modal> </a-modal>
@ -56,6 +60,7 @@
switch (protocol) { switch (protocol) {
case Protocols.TROJAN: return client.password; case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email; case Protocols.SHADOWSOCKS: return client.email;
case Protocols.HYSTERIA: return client.auth;
default: return client.id; default: return client.id;
} }
}, },
@ -65,6 +70,7 @@
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS()); case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan()); case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method))); case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
case Protocols.HYSTERIA: return clients.push(new Inbound.HysteriaSettings.Hysteria());
default: return null; default: return null;
} }
}, },

View file

@ -270,6 +270,10 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
if client.Email == "" { if client.Email == "" {
return inbound, false, common.NewError("empty client ID") return inbound, false, common.NewError("empty client ID")
} }
case "hysteria":
if client.Auth == "" {
return inbound, false, common.NewError("empty client ID")
}
default: default:
if client.ID == "" { if client.ID == "" {
return inbound, false, common.NewError("empty client ID") return inbound, false, common.NewError("empty client ID")
@ -606,6 +610,10 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
if client.Email == "" { if client.Email == "" {
return false, common.NewError("empty client ID") return false, common.NewError("empty client ID")
} }
case "hysteria":
if client.Auth == "" {
return false, common.NewError("empty client ID")
}
default: default:
if client.ID == "" { if client.ID == "" {
return false, common.NewError("empty client ID") return false, common.NewError("empty client ID")
@ -655,6 +663,7 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{ err1 := s.xrayApi.AddUser(string(oldInbound.Protocol), oldInbound.Tag, map[string]any{
"email": client.Email, "email": client.Email,
"id": client.ID, "id": client.ID,
"auth": client.Auth,
"security": client.Security, "security": client.Security,
"flow": client.Flow, "flow": client.Flow,
"password": client.Password, "password": client.Password,
@ -690,11 +699,13 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
email := "" email := ""
client_key := "id" client_key := "id"
if oldInbound.Protocol == "trojan" { switch oldInbound.Protocol {
case "trojan":
client_key = "password" client_key = "password"
} case "shadowsocks":
if oldInbound.Protocol == "shadowsocks" {
client_key = "email" client_key = "email"
case "hysteria":
client_key = "auth"
} }
interfaceClients := settings["clients"].([]any) interfaceClients := settings["clients"].([]any)
@ -801,6 +812,9 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
case "shadowsocks": case "shadowsocks":
oldClientId = oldClient.Email oldClientId = oldClient.Email
newClientId = clients[0].Email newClientId = clients[0].Email
case "hysteria":
oldClientId = oldClient.Auth
newClientId = clients[0].Auth
default: default:
oldClientId = oldClient.ID oldClientId = oldClient.ID
newClientId = clients[0].ID newClientId = clients[0].ID
@ -921,6 +935,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
"id": clients[0].ID, "id": clients[0].ID,
"security": clients[0].Security, "security": clients[0].Security,
"flow": clients[0].Flow, "flow": clients[0].Flow,
"auth": clients[0].Auth,
"password": clients[0].Password, "password": clients[0].Password,
"cipher": cipher, "cipher": cipher,
}) })
@ -1813,6 +1828,7 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) (bool, e
err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{ err1 := s.xrayApi.AddUser(string(inbound.Protocol), inbound.Tag, map[string]any{
"email": client.Email, "email": client.Email,
"id": client.ID, "id": client.ID,
"auth": client.Auth,
"security": client.Security, "security": client.Security,
"flow": client.Flow, "flow": client.Flow,
"password": client.Password, "password": client.Password,

View file

@ -148,10 +148,10 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
// clear client config for additional parameters // 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" && key != "auth" {
delete(c, key) delete(c, key)
} }
if c["flow"] == "xtls-rprx-vision-udp443" { if flow, ok := c["flow"].(string); ok && flow == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision" c["flow"] = "xtls-rprx-vision"
} }
} }

View file

@ -19,6 +19,7 @@ import (
"github.com/xtls/xray-core/common/protocol" "github.com/xtls/xray-core/common/protocol"
"github.com/xtls/xray-core/common/serial" "github.com/xtls/xray-core/common/serial"
"github.com/xtls/xray-core/infra/conf" "github.com/xtls/xray-core/infra/conf"
hysteriaAccount "github.com/xtls/xray-core/proxy/hysteria/account"
"github.com/xtls/xray-core/proxy/shadowsocks" "github.com/xtls/xray-core/proxy/shadowsocks"
"github.com/xtls/xray-core/proxy/shadowsocks_2022" "github.com/xtls/xray-core/proxy/shadowsocks_2022"
"github.com/xtls/xray-core/proxy/trojan" "github.com/xtls/xray-core/proxy/trojan"
@ -167,6 +168,10 @@ func (x *XrayAPI) AddUser(Protocol string, inboundTag string, user map[string]an
Email: user["email"].(string), Email: user["email"].(string),
}) })
} }
case "hysteria":
account = serial.ToTypedMessage(&hysteriaAccount.Account{
Auth: user["auth"].(string),
})
default: default:
return nil return nil
} }