old design
|
@ -1 +1 @@
|
||||||
1.1.1
|
1.0.9
|
||||||
|
|
|
@ -73,11 +73,10 @@ type Setting struct {
|
||||||
|
|
||||||
type Client struct {
|
type Client struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Password string `json:"password"`
|
|
||||||
Flow string `json:"flow"`
|
|
||||||
AlterIds uint16 `json:"alterId"`
|
AlterIds uint16 `json:"alterId"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
LimitIP int `json:"limitIp"`
|
LimitIP int `json:"limitIp"`
|
||||||
|
Security string `json:"security"`
|
||||||
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
TotalGB int64 `json:"totalGB" form:"totalGB"`
|
||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||||
}
|
}
|
||||||
|
|
10
main.go
|
@ -136,7 +136,7 @@ func updateTgbotEnableSts(status bool) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
|
func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
|
||||||
err := database.InitDB(config.GetDBPath())
|
err := database.InitDB(config.GetDBPath())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -165,7 +165,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime stri
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if tgBotChatid != "" {
|
if tgBotChatid != 0 {
|
||||||
err := settingService.SetTgBotChatId(tgBotChatid)
|
err := settingService.SetTgBotChatId(tgBotChatid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
@ -224,7 +224,7 @@ func main() {
|
||||||
var username string
|
var username string
|
||||||
var password string
|
var password string
|
||||||
var tgbottoken string
|
var tgbottoken string
|
||||||
var tgbotchatid string
|
var tgbotchatid int
|
||||||
var enabletgbot bool
|
var enabletgbot bool
|
||||||
var tgbotRuntime string
|
var tgbotRuntime string
|
||||||
var reset bool
|
var reset bool
|
||||||
|
@ -236,7 +236,7 @@ func main() {
|
||||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
||||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
||||||
settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegrame bot chat id")
|
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
|
||||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||||
|
|
||||||
oldUsage := flag.Usage
|
oldUsage := flag.Usage
|
||||||
|
@ -287,7 +287,7 @@ func main() {
|
||||||
if show {
|
if show {
|
||||||
showSetting(show)
|
showSetting(show)
|
||||||
}
|
}
|
||||||
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
|
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
|
||||||
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|
BIN
media/1.png
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 47 KiB |
BIN
media/2.png
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 49 KiB |
BIN
media/3.png
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 42 KiB |
BIN
media/4.png
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 28 KiB |
|
@ -1,4 +1,3 @@
|
||||||
//go:build darwin
|
|
||||||
// +build darwin
|
// +build darwin
|
||||||
|
|
||||||
package sys
|
package sys
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
//go:build linux
|
|
||||||
// +build linux
|
// +build linux
|
||||||
|
|
||||||
package sys
|
package sys
|
||||||
|
|
2
web/assets/ant-design-vue@1.7.2/antd.min.css
vendored
|
@ -156,6 +156,16 @@
|
||||||
padding:16px;
|
padding:16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-menu-dark,
|
||||||
|
.ant-menu-dark .ant-menu-sub,
|
||||||
|
.ant-layout-header,
|
||||||
|
.ant-layout-sider-dark,
|
||||||
|
.ant-layout-sider-zero-width-trigger,
|
||||||
|
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
|
||||||
|
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
|
||||||
|
background:#161b22
|
||||||
|
}
|
||||||
|
|
||||||
.ant-card-dark {
|
.ant-card-dark {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #1a212a;
|
background-color: #1a212a;
|
||||||
|
@ -178,9 +188,10 @@
|
||||||
|
|
||||||
.ant-card-dark .ant-collapse-content,
|
.ant-card-dark .ant-collapse-content,
|
||||||
.ant-card-dark .ant-calendar,
|
.ant-card-dark .ant-calendar,
|
||||||
.ant-card-dark .ant-table-placeholder {
|
.ant-card-dark .ant-table-placeholder,
|
||||||
|
.ant-card-dark .ant-input-group-addon {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #1a212a;
|
background-color: #262f3d;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-list-item-meta-title,
|
.ant-card-dark .ant-list-item-meta-title,
|
||||||
|
@ -198,7 +209,8 @@
|
||||||
.ant-card-dark .ant-calendar-year-select,
|
.ant-card-dark .ant-calendar-year-select,
|
||||||
.ant-card-dark .ant-calendar-date,
|
.ant-card-dark .ant-calendar-date,
|
||||||
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
|
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
|
||||||
.ant-card-dark .ant-empty-normal {
|
.ant-card-dark .ant-empty-normal,
|
||||||
|
.ant-card-dark .ant-checkbox+span {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -210,7 +222,7 @@
|
||||||
|
|
||||||
.ant-card-dark tbody .ant-table-expanded-row {
|
.ant-card-dark tbody .ant-table-expanded-row {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #023366;
|
background-color: #1a212a;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-input,
|
.ant-card-dark .ant-input,
|
||||||
|
@ -219,7 +231,7 @@
|
||||||
.ant-card-dark .ant-select-dropdown-menu-item-selected,
|
.ant-card-dark .ant-select-dropdown-menu-item-selected,
|
||||||
.ant-card-dark .ant-select-selection {
|
.ant-card-dark .ant-select-selection {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #023366;
|
background-color: #2e3b52;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark .ant-collapse-item {
|
.ant-card-dark .ant-collapse-item {
|
||||||
|
@ -232,7 +244,7 @@
|
||||||
.ant-card-dark .ant-modal-header,
|
.ant-card-dark .ant-modal-header,
|
||||||
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
background-color: #242c3a;
|
background-color: #222a37;
|
||||||
}
|
}
|
||||||
|
|
||||||
.client-table-header {
|
.client-table-header {
|
||||||
|
@ -244,7 +256,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark .client-table-header {
|
.ant-card-dark .client-table-header {
|
||||||
background-color: #023366;
|
background-color: #1a212a;
|
||||||
color: hsla(0,0%,100%,.65);
|
color: hsla(0,0%,100%,.65);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -267,3 +279,61 @@
|
||||||
background-color: #1a212a;
|
background-color: #1a212a;
|
||||||
border: 1px solid hsla(0,0%,100%,.30);
|
border: 1px solid hsla(0,0%,100%,.30);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-blue {
|
||||||
|
color: #3c9ae8;
|
||||||
|
background: #111d2c;
|
||||||
|
border-color: #15395b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-green {
|
||||||
|
color: #6abe39;
|
||||||
|
background: #162312;
|
||||||
|
border-color: #274916;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-cyan {
|
||||||
|
color: #33bcb7;
|
||||||
|
background: #112123;
|
||||||
|
border-color: #144848;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-red {
|
||||||
|
color: #e84749;
|
||||||
|
background: #2a1215;
|
||||||
|
border-color: #58181c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-tag-orange {
|
||||||
|
color: #e89a3c;
|
||||||
|
background: #2b1d11;
|
||||||
|
border-color: #593815;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-table-row-expand-icon,
|
||||||
|
.ant-card-dark .ant-checkbox-inner {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-switch-checked {
|
||||||
|
background-color: #0c61b0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-btn,
|
||||||
|
.ant-card-dark .ant-radio-button-wrapper {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background: none;
|
||||||
|
border: 1px solid hsla(0,0%,100%,.65);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-radio-button-wrapper:hover {
|
||||||
|
color: #177ddc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-card-dark .ant-btn-primary {
|
||||||
|
color: hsla(0,0%,100%,.65);
|
||||||
|
background-color: #073763;
|
||||||
|
border-color: #1890ff;
|
||||||
|
text-shadow: 0 -1px 0 rgba(0,0,0,.12);
|
||||||
|
box-shadow: 0 2px 0 rgba(0,0,0,.045);
|
||||||
|
}
|
|
@ -36,8 +36,7 @@ class DBInbound {
|
||||||
this.remark = "";
|
this.remark = "";
|
||||||
this.enable = true;
|
this.enable = true;
|
||||||
this.expiryTime = 0;
|
this.expiryTime = 0;
|
||||||
this.limitIp = 0;
|
this.iplimit = 0;
|
||||||
|
|
||||||
this.listen = "";
|
this.listen = "";
|
||||||
this.port = 0;
|
this.port = 0;
|
||||||
this.protocol = "";
|
this.protocol = "";
|
||||||
|
@ -110,6 +109,10 @@ class DBInbound {
|
||||||
get isExpiry() {
|
get isExpiry() {
|
||||||
return this.expiryTime < new Date().getTime();
|
return this.expiryTime < new Date().getTime();
|
||||||
}
|
}
|
||||||
|
get isDBInboundEmpty() {
|
||||||
|
const inbound = this.toInbound();
|
||||||
|
return inbound.isInboundEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
toInbound() {
|
toInbound() {
|
||||||
let settings = {};
|
let settings = {};
|
||||||
|
@ -156,7 +159,6 @@ class DBInbound {
|
||||||
const inbound = this.toInbound();
|
const inbound = this.toInbound();
|
||||||
return inbound.genLink(this.address, this.remark, clientIndex);
|
return inbound.genLink(this.address, this.remark, clientIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
get genInboundLinks() {
|
get genInboundLinks() {
|
||||||
const inbound = this.toInbound();
|
const inbound = this.toInbound();
|
||||||
return inbound.genInboundLinks(this.address, this.remark);
|
return inbound.genInboundLinks(this.address, this.remark);
|
||||||
|
@ -173,12 +175,8 @@ class AllSetting {
|
||||||
this.webBasePath = "/";
|
this.webBasePath = "/";
|
||||||
this.tgBotEnable = false;
|
this.tgBotEnable = false;
|
||||||
this.tgBotToken = "";
|
this.tgBotToken = "";
|
||||||
this.tgBotChatId = "";
|
this.tgBotChatId = 0;
|
||||||
this.tgRunTime = "@daily";
|
this.tgRunTime = "";
|
||||||
this.tgBotBackup = false;
|
|
||||||
this.tgExpireDiff = "";
|
|
||||||
this.tgTrafficDiff = "";
|
|
||||||
this.tgCpu = "";
|
|
||||||
this.xrayTemplateConfig = "";
|
this.xrayTemplateConfig = "";
|
||||||
|
|
||||||
this.timeLocation = "Asia/Tehran";
|
this.timeLocation = "Asia/Tehran";
|
||||||
|
|
|
@ -95,7 +95,6 @@ const UTLS_FINGERPRINT = {
|
||||||
const ALPN_OPTION = {
|
const ALPN_OPTION = {
|
||||||
H2: "h2",
|
H2: "h2",
|
||||||
HTTP1: "http/1.1",
|
HTTP1: "http/1.1",
|
||||||
BOTH: "h2,http/1.1",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.freeze(Protocols);
|
Object.freeze(Protocols);
|
||||||
|
@ -476,7 +475,7 @@ class GrpcStreamSettings extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
class TlsStreamSettings extends XrayCommonClass {
|
class TlsStreamSettings extends XrayCommonClass {
|
||||||
constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS12, maxVersion = TLS_VERSION_OPTION.TLS13,
|
constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS10, maxVersion = TLS_VERSION_OPTION.TLS13,
|
||||||
cipherSuites = '',
|
cipherSuites = '',
|
||||||
certificates = [new TlsStreamSettings.Cert()], alpn=[''] ,settings=[new TlsStreamSettings.Settings()]) {
|
certificates = [new TlsStreamSettings.Cert()], alpn=[''] ,settings=[new TlsStreamSettings.Settings()]) {
|
||||||
super();
|
super();
|
||||||
|
@ -575,9 +574,9 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
|
||||||
};
|
};
|
||||||
|
|
||||||
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||||
constructor(insecure = false, fingerprint = '', serverName = '') {
|
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
|
||||||
super();
|
super();
|
||||||
this.inSecure = insecure;
|
this.allowInsecure = allowInsecure;
|
||||||
this.fingerprint = fingerprint;
|
this.fingerprint = fingerprint;
|
||||||
this.serverName = serverName;
|
this.serverName = serverName;
|
||||||
}
|
}
|
||||||
|
@ -590,7 +589,7 @@ TlsStreamSettings.Settings = class extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
return {
|
||||||
allowInsecure: this.inSecure,
|
allowInsecure: this.allowInsecure,
|
||||||
fingerprint: this.fingerprint,
|
fingerprint: this.fingerprint,
|
||||||
serverName: this.serverName,
|
serverName: this.serverName,
|
||||||
};
|
};
|
||||||
|
@ -1084,7 +1083,8 @@ class Inbound extends XrayCommonClass {
|
||||||
tls: this.stream.security,
|
tls: this.stream.security,
|
||||||
sni: this.stream.tls.settings[0]['serverName'],
|
sni: this.stream.tls.settings[0]['serverName'],
|
||||||
fp: this.stream.tls.settings[0]['fingerprint'],
|
fp: this.stream.tls.settings[0]['fingerprint'],
|
||||||
alpn: this.stream.tls.alpn[0],
|
alpn: this.stream.tls.alpn.join(','),
|
||||||
|
allowInsecure: this.stream.tls.settings[0].allowInsecure,
|
||||||
};
|
};
|
||||||
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
|
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
|
||||||
}
|
}
|
||||||
|
@ -1096,7 +1096,6 @@ class Inbound extends XrayCommonClass {
|
||||||
const type = this.stream.network;
|
const type = this.stream.network;
|
||||||
const params = new Map();
|
const params = new Map();
|
||||||
params.set("type", this.stream.network);
|
params.set("type", this.stream.network);
|
||||||
params.set("security", this.stream.security);
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
const tcp = this.stream.tcp;
|
const tcp = this.stream.tcp;
|
||||||
|
@ -1143,8 +1142,12 @@ class Inbound extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tls) {
|
if (this.tls) {
|
||||||
|
params.set("security", "tls");
|
||||||
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
||||||
params.set("alpn", this.stream.tls.alpn[0]);
|
params.set("alpn", this.stream.tls.alpn);
|
||||||
|
if(this.stream.tls.settings[0].allowInsecure){
|
||||||
|
params.set("allowInsecure", "1");
|
||||||
|
}
|
||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
}
|
||||||
|
@ -1157,6 +1160,11 @@ class Inbound extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.XTLS) {
|
if (this.XTLS) {
|
||||||
|
params.set("security", "tls");
|
||||||
|
params.set("alpn", this.stream.tls.alpn);
|
||||||
|
if(this.stream.tls.settings[0].allowInsecure){
|
||||||
|
params.set("allowInsecure", "1");
|
||||||
|
}
|
||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
}
|
||||||
|
@ -1192,7 +1200,6 @@ class Inbound extends XrayCommonClass {
|
||||||
const type = this.stream.network;
|
const type = this.stream.network;
|
||||||
const params = new Map();
|
const params = new Map();
|
||||||
params.set("type", this.stream.network);
|
params.set("type", this.stream.network);
|
||||||
params.set("security", this.stream.security);
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
const tcp = this.stream.tcp;
|
const tcp = this.stream.tcp;
|
||||||
|
@ -1239,8 +1246,12 @@ class Inbound extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.tls) {
|
if (this.tls) {
|
||||||
|
params.set("security", "tls");
|
||||||
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
|
||||||
params.set("alpn", this.stream.tls.alpn[0]);
|
params.set("alpn", this.stream.tls.alpn);
|
||||||
|
if(this.stream.tls.settings[0].allowInsecure){
|
||||||
|
params.set("allowInsecure", "1");
|
||||||
|
}
|
||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
}
|
||||||
|
@ -1250,6 +1261,11 @@ class Inbound extends XrayCommonClass {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.XTLS) {
|
if (this.XTLS) {
|
||||||
|
params.set("security", "tls");
|
||||||
|
params.set("alpn", this.stream.tls.alpn);
|
||||||
|
if(this.stream.tls.settings[0].allowInsecure){
|
||||||
|
params.set("allowInsecure", "1");
|
||||||
|
}
|
||||||
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
|
||||||
address = this.stream.tls.server;
|
address = this.stream.tls.server;
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,10 +33,7 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
g.POST("/update/:id", a.updateInbound)
|
g.POST("/update/:id", a.updateInbound)
|
||||||
g.POST("/clientIps/:email", a.getClientIps)
|
g.POST("/clientIps/:email", a.getClientIps)
|
||||||
g.POST("/clearClientIps/:email", a.clearClientIps)
|
g.POST("/clearClientIps/:email", a.clearClientIps)
|
||||||
g.POST("/addClient/", a.addInboundClient)
|
g.POST("/resetClientTraffic/:email", a.resetClientTraffic)
|
||||||
g.POST("/delClient/:email", a.delInboundClient)
|
|
||||||
g.POST("/updateClient/:index", a.updateInboundClient)
|
|
||||||
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,7 +124,6 @@ func (a *InboundController) updateInbound(c *gin.Context) {
|
||||||
a.xrayService.SetToNeedRestart()
|
a.xrayService.SetToNeedRestart()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) getClientIps(c *gin.Context) {
|
func (a *InboundController) getClientIps(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
|
@ -148,85 +144,13 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
|
||||||
}
|
}
|
||||||
jsonMsg(c, "Log Cleared", nil)
|
jsonMsg(c, "Log Cleared", nil)
|
||||||
}
|
}
|
||||||
func (a *InboundController) addInboundClient(c *gin.Context) {
|
|
||||||
inbound := &model.Inbound{}
|
|
||||||
err := c.ShouldBind(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.inboundService.AddInboundClient(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "something worng!", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, "Client added", nil)
|
|
||||||
if err == nil {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *InboundController) delInboundClient(c *gin.Context) {
|
|
||||||
email := c.Param("email")
|
|
||||||
inbound := &model.Inbound{}
|
|
||||||
err := c.ShouldBind(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.inboundService.DelInboundClient(inbound, email)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "something worng!", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, "Client deleted", nil)
|
|
||||||
if err == nil {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *InboundController) updateInboundClient(c *gin.Context) {
|
|
||||||
index, err := strconv.Atoi(c.Param("index"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
inbound := &model.Inbound{}
|
|
||||||
err = c.ShouldBind(inbound)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.inboundService.UpdateInboundClient(inbound, index)
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, "something worng!", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsonMsg(c, "Client updated", nil)
|
|
||||||
if err == nil {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
func (a *InboundController) resetClientTraffic(c *gin.Context) {
|
||||||
id, err := strconv.Atoi(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
err = a.inboundService.ResetClientTraffic(id, email)
|
err := a.inboundService.ResetClientTraffic(email)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
jsonMsg(c, "something worng!", err)
|
jsonMsg(c, "something worng!", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
jsonMsg(c, "traffic reseted", nil)
|
jsonMsg(c, "traffic reseted", nil)
|
||||||
if err == nil {
|
|
||||||
a.xrayService.SetToNeedRestart()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
|
"x-ui/web/job"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
|
@ -19,7 +20,6 @@ type IndexController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
userService service.UserService
|
userService service.UserService
|
||||||
tgbot service.Tgbot
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||||
|
@ -60,13 +60,13 @@ func (a *IndexController) login(c *gin.Context) {
|
||||||
user := a.userService.CheckUser(form.Username, form.Password)
|
user := a.userService.CheckUser(form.Username, form.Password)
|
||||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||||
if user == nil {
|
if user == nil {
|
||||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||||
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||||
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
|
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
|
||||||
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = session.SetLoginUser(c, user)
|
err = session.SetLoginUser(c, user)
|
||||||
|
|
|
@ -2,12 +2,11 @@ package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/web/entity"
|
"x-ui/web/entity"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"x-ui/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type updateUserForm struct {
|
type updateUserForm struct {
|
||||||
|
|
|
@ -34,13 +34,10 @@ type AllSetting struct {
|
||||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||||
TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
|
TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
|
||||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||||
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
|
|
||||||
TgExpireDiff int `json:"tgExpireDiff" form:"tgExpireDiff"`
|
|
||||||
TgTrafficDiff int `json:"tgTrafficDiff" form:"tgTrafficDiff"`
|
|
||||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
|
||||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||||
|
|
||||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,8 +3,7 @@
|
||||||
:closable="true" width="300px" :ok-text="qrModal.okText"
|
:closable="true" width="300px" :ok-text="qrModal.okText"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
||||||
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
|
||||||
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
|
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
||||||
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
|
|
||||||
</a-modal>
|
</a-modal>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -32,13 +31,12 @@
|
||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
qrModalApp.$nextTick(() => {
|
qrModalApp.$nextTick(() => {
|
||||||
|
if (this.clipboard === null) {
|
||||||
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
|
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
|
||||||
text: () => this.copyText,
|
text: () => this.copyText,
|
||||||
});
|
});
|
||||||
this.clipboard.on('success', () => {
|
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||||
app.$message.success('{{ i18n "copied" }}')
|
}
|
||||||
this.clipboard.destroy();
|
|
||||||
});
|
|
||||||
if (this.qrcode === null) {
|
if (this.qrcode === null) {
|
||||||
this.qrcode = new QRious({
|
this.qrcode = new QRious({
|
||||||
element: document.querySelector('#qrCode'),
|
element: document.querySelector('#qrCode'),
|
||||||
|
@ -60,17 +58,6 @@
|
||||||
data: {
|
data: {
|
||||||
qrModal: qrModal,
|
qrModal: qrModal,
|
||||||
},
|
},
|
||||||
methods: {
|
|
||||||
copyToClipboard() {
|
|
||||||
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
|
|
||||||
text: () => this.qrModal.copyText,
|
|
||||||
});
|
|
||||||
this.qrModal.clipboard.on('success', () => {
|
|
||||||
app.$message.success('{{ i18n "copied" }}')
|
|
||||||
this.qrModal.clipboard.destroy();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -32,6 +32,7 @@
|
||||||
});
|
});
|
||||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
close: function () {
|
close: function () {
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-row type="flex" justify="center">
|
<a-row type="flex" justify="center">
|
||||||
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
|
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
|
||||||
<h1>{{ i18n "pages.login.title" }}</h1>
|
<h1>3x-ui {{ i18n "pages.login.title" }}</h1>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
<a-row type="flex" justify="center">
|
<a-row type="flex" justify="center">
|
||||||
|
|
|
@ -1,160 +0,0 @@
|
||||||
{{define "clientsBulkModal"}}
|
|
||||||
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
|
|
||||||
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
|
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
|
||||||
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
|
|
||||||
<a-form layout="inline">
|
|
||||||
<a-form-item label='{{ i18n "pages.client.method" }}'>
|
|
||||||
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
|
||||||
<a-select-option :value="0">Random</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="3">Random_Prefix+Num+Postfix</a-select-option>
|
|
||||||
<a-select-option :value="4">Random_Prefix+Num@Telegram Username</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item><br />
|
|
||||||
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
|
||||||
<span slot="label">{{ i18n "pages.client.first" }}</span>
|
|
||||||
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="clientsBulkModal.emailMethod>1">
|
|
||||||
<span slot="label">{{ i18n "pages.client.last" }}</span>
|
|
||||||
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="clientsBulkModal.emailMethod>0">
|
|
||||||
<span slot="label">{{ i18n "pages.client.prefix" }}</span>
|
|
||||||
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="clientsBulkModal.emailMethod>2">
|
|
||||||
<span slot="label" v-if="clientsBulkModal.emailMethod == 4">tg_uname</span>
|
|
||||||
<span slot="label" v-else>{{ i18n "pages.client.postfix" }}</span>
|
|
||||||
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
|
|
||||||
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
|
|
||||||
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
|
|
||||||
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
|
||||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
|
||||||
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
</a-modal>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
const clientsBulkModal = {
|
|
||||||
visible: false,
|
|
||||||
confirmLoading: false,
|
|
||||||
title: '',
|
|
||||||
okText: '',
|
|
||||||
confirm: null,
|
|
||||||
dbInbound: new DBInbound(),
|
|
||||||
inbound: new Inbound(),
|
|
||||||
clients: [],
|
|
||||||
quantity: 1,
|
|
||||||
totalGB: 0,
|
|
||||||
expiryTime: '',
|
|
||||||
emailMethod: 0,
|
|
||||||
firstNum: 1,
|
|
||||||
lastNum: 1,
|
|
||||||
emailPrefix: "",
|
|
||||||
emailPostfix: "",
|
|
||||||
ok() {
|
|
||||||
method=clientsBulkModal.emailMethod;
|
|
||||||
if(method>1){
|
|
||||||
start=clientsBulkModal.firstNum;
|
|
||||||
end=clientsBulkModal.lastNum + 1;
|
|
||||||
} else {
|
|
||||||
start=0;
|
|
||||||
end=clientsBulkModal.quantity;
|
|
||||||
}
|
|
||||||
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? "_" + clientsBulkModal.emailPrefix : "";
|
|
||||||
useNum=(method>1);
|
|
||||||
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
|
|
||||||
for (let i = start; i < end; i++) {
|
|
||||||
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
|
|
||||||
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
|
|
||||||
newClient._totalGB = clientsBulkModal.totalGB;
|
|
||||||
newClient._expiryTime = clientsBulkModal.expiryTime;
|
|
||||||
clientsBulkModal.clients.push(newClient);
|
|
||||||
}
|
|
||||||
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
|
|
||||||
},
|
|
||||||
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
|
|
||||||
this.visible = true;
|
|
||||||
this.title = title;
|
|
||||||
this.okText = okText;
|
|
||||||
this.confirm = confirm;
|
|
||||||
this.quantity = 1;
|
|
||||||
this.totalGB = 0;
|
|
||||||
this.expiryTime = '';
|
|
||||||
this.emailMethod= 0;
|
|
||||||
this.firstNum= 1;
|
|
||||||
this.lastNum= 1;
|
|
||||||
this.emailPrefix= "";
|
|
||||||
this.emailPostfix= "";
|
|
||||||
|
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
|
||||||
this.inbound = dbInbound.toInbound();
|
|
||||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
|
||||||
},
|
|
||||||
getClients(protocol, clientSettings) {
|
|
||||||
switch(protocol){
|
|
||||||
case Protocols.VMESS: return clientSettings.vmesses;
|
|
||||||
case Protocols.VLESS: return clientSettings.vlesses;
|
|
||||||
case Protocols.TROJAN: return clientSettings.trojans;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
newClient(protocol) {
|
|
||||||
switch (protocol) {
|
|
||||||
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
|
|
||||||
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
|
|
||||||
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
clientsBulkModal.visible = false;
|
|
||||||
clientsBulkModal.loading(false);
|
|
||||||
},
|
|
||||||
loading(loading) {
|
|
||||||
clientsBulkModal.confirmLoading = loading;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientsBulkModalApp = new Vue({
|
|
||||||
delimiters: ['[[', ']]'],
|
|
||||||
el: '#client-bulk-modal',
|
|
||||||
data: {
|
|
||||||
clientsBulkModal,
|
|
||||||
get inbound() {
|
|
||||||
return this.clientsBulkModal.inbound;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{{end}}
|
|
|
@ -1,133 +0,0 @@
|
||||||
{{define "clientsModal"}}
|
|
||||||
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
|
|
||||||
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
|
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
|
||||||
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
|
|
||||||
{{template "form/client"}}
|
|
||||||
</a-modal>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
const clientModal = {
|
|
||||||
visible: false,
|
|
||||||
confirmLoading: false,
|
|
||||||
title: '',
|
|
||||||
okText: '',
|
|
||||||
dbInbound: new DBInbound(),
|
|
||||||
inbound: new Inbound(),
|
|
||||||
clients: [],
|
|
||||||
clientStats: [],
|
|
||||||
index: null,
|
|
||||||
clientIps: null,
|
|
||||||
isExpired: false,
|
|
||||||
ok() {
|
|
||||||
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
|
|
||||||
},
|
|
||||||
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=(index, dbInbound)=>{}, isEdit=false }) {
|
|
||||||
this.visible = true;
|
|
||||||
this.title = title;
|
|
||||||
this.okText = okText;
|
|
||||||
this.isEdit = isEdit;
|
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
|
||||||
this.inbound = dbInbound.toInbound();
|
|
||||||
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
|
|
||||||
this.index = index === null ? this.clients.length : index;
|
|
||||||
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
|
|
||||||
if (!isEdit){
|
|
||||||
this.addClient(this.inbound.protocol, this.clients);
|
|
||||||
}
|
|
||||||
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
|
|
||||||
this.confirm = confirm;
|
|
||||||
},
|
|
||||||
getClients(protocol, clientSettings) {
|
|
||||||
switch(protocol){
|
|
||||||
case Protocols.VMESS: return clientSettings.vmesses;
|
|
||||||
case Protocols.VLESS: return clientSettings.vlesses;
|
|
||||||
case Protocols.TROJAN: return clientSettings.trojans;
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
addClient(protocol, clients) {
|
|
||||||
switch (protocol) {
|
|
||||||
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
|
|
||||||
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
|
|
||||||
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
|
|
||||||
default: return null;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
close() {
|
|
||||||
clientModal.visible = false;
|
|
||||||
clientModal.loading(false);
|
|
||||||
},
|
|
||||||
loading(loading) {
|
|
||||||
clientModal.confirmLoading = loading;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientModalApp = new Vue({
|
|
||||||
delimiters: ['[[', ']]'],
|
|
||||||
el: '#client-modal',
|
|
||||||
data: {
|
|
||||||
clientModal,
|
|
||||||
get inbound() {
|
|
||||||
return this.clientModal.inbound;
|
|
||||||
},
|
|
||||||
get client() {
|
|
||||||
return this.clientModal.clients[this.clientModal.index];
|
|
||||||
},
|
|
||||||
get clientStats() {
|
|
||||||
return this.clientModal.clientStats;
|
|
||||||
},
|
|
||||||
get isEdit() {
|
|
||||||
return this.clientModal.isEdit;
|
|
||||||
},
|
|
||||||
get isTrafficExhausted() {
|
|
||||||
if(!clientStats) return false
|
|
||||||
if(clientStats.total == 0) return false
|
|
||||||
if(clientStats.up + clientStats.down < clientStats.total) return false
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
get isExpiry() {
|
|
||||||
return this.clientModal.isExpired
|
|
||||||
},
|
|
||||||
get statsColor() {
|
|
||||||
if(!clientStats) return 'blue'
|
|
||||||
if(clientStats.total === 0) return 'blue'
|
|
||||||
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
|
|
||||||
else return 'red'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
getNewEmail(client) {
|
|
||||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
|
||||||
var string = '';
|
|
||||||
var len = 6 + Math.floor(Math.random() * 5);
|
|
||||||
for(var ii=0; ii<len; ii++){
|
|
||||||
string += chars[Math.floor(Math.random() * chars.length)];
|
|
||||||
}
|
|
||||||
client.email = string;
|
|
||||||
},
|
|
||||||
async getDBClientIps(email,event) {
|
|
||||||
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
|
|
||||||
if (!msg.success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
ips = JSON.parse(msg.obj)
|
|
||||||
ips = ips.join(",")
|
|
||||||
event.target.value = ips
|
|
||||||
} catch (error) {
|
|
||||||
// text
|
|
||||||
event.target.value = msg.obj
|
|
||||||
}
|
|
||||||
},
|
|
||||||
async clearDBClientIps(email) {
|
|
||||||
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
|
|
||||||
if (!msg.success) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
document.getElementById("clientIPs").value = ""
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{{end}}
|
|
|
@ -13,7 +13,7 @@
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
<!--<a-menu-item key="{{ .base_path }}xui/clients">-->
|
||||||
<!-- <a-icon type="laptop"></a-icon>-->
|
<!-- <a-icon type="laptop"></a-icon>-->
|
||||||
<!-- <span>Client</span>-->
|
<!-- <span>client</span>-->
|
||||||
<!--</a-menu-item>-->
|
<!--</a-menu-item>-->
|
||||||
<a-sub-menu>
|
<a-sub-menu>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
|
@ -41,7 +41,7 @@
|
||||||
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
||||||
<a-menu-item mode="inline">
|
<a-menu-item mode="inline">
|
||||||
<a-icon type="bg-colors"></a-icon>
|
<a-icon type="bg-colors"></a-icon>
|
||||||
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
<a-switch size="small" :default-checked="siderDrawer.isDarkTheme"
|
||||||
checked-children="☀"
|
checked-children="☀"
|
||||||
un-checked-children="🌙"
|
un-checked-children="🌙"
|
||||||
@change="siderDrawer.changeTheme()"></a-switch>
|
@change="siderDrawer.changeTheme()"></a-switch>
|
||||||
|
@ -55,12 +55,11 @@
|
||||||
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
<a-drawer id="sider-drawer" placement="left" :closable="false"
|
||||||
@close="siderDrawer.close()"
|
@close="siderDrawer.close()"
|
||||||
:visible="siderDrawer.visible"
|
:visible="siderDrawer.visible"
|
||||||
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
|
|
||||||
:wrap-style="{ padding: 0 }">
|
:wrap-style="{ padding: 0 }">
|
||||||
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
|
||||||
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
|
||||||
</div>
|
</div>
|
||||||
<a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
|
<a-menu mode="inline" selected-keys="">
|
||||||
<a-menu-item mode="inline">
|
<a-menu-item mode="inline">
|
||||||
<a-icon type="bg-colors"></a-icon>
|
<a-icon type="bg-colors"></a-icon>
|
||||||
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
<a-switch :default-checked="siderDrawer.isDarkTheme"
|
||||||
|
@ -69,12 +68,14 @@
|
||||||
@change="siderDrawer.changeTheme()"></a-switch>
|
@change="siderDrawer.changeTheme()"></a-switch>
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
<a-menu mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||||
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
|
||||||
{{template "menuItems" .}}
|
{{template "menuItems" .}}
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-drawer>
|
</a-drawer>
|
||||||
<script>
|
<script>
|
||||||
|
|
||||||
|
|
||||||
const darkClass = "ant-card-dark";
|
const darkClass = "ant-card-dark";
|
||||||
const bgDarkStyle = "background-color: #242c3a";
|
const bgDarkStyle = "background-color: #242c3a";
|
||||||
const siderDrawer = {
|
const siderDrawer = {
|
||||||
|
|
|
@ -1,110 +0,0 @@
|
||||||
{{define "form/client"}}
|
|
||||||
<a-form layout="inline" v-if="client">
|
|
||||||
<template v-if="isEdit">
|
|
||||||
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
|
||||||
</template>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
Email
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
The Email Must Be Completely Unique
|
|
||||||
</template>
|
|
||||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN">
|
|
||||||
<a-input v-model.trim="client.password" style="width: 150px;" ></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
|
|
||||||
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
|
|
||||||
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
IP Count Limit
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
Disable inbound if more than entered count (0 for disable limit ip)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
|
|
||||||
<span slot="label">
|
|
||||||
IP Log
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
Clear The Log
|
|
||||||
</template>
|
|
||||||
<span style="color: #FF4D4F">
|
|
||||||
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
|
|
||||||
</span>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-form layout="block">
|
|
||||||
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
|
||||||
</a-textarea>
|
|
||||||
</a-form>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-if="inbound.XTLS" label="Flow">
|
|
||||||
<a-select v-model="client.flow" style="width: 150px">
|
|
||||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
|
||||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
|
|
||||||
<a-select v-model="client.flow" style="width: 150px">
|
|
||||||
<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-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-input-number v-model="client._totalGB":min="0" style="width: 70px;"></a-input-number>
|
|
||||||
<template v-if="isEdit && clientStats">
|
|
||||||
{{ i18n "usage" }}:
|
|
||||||
<a-tag :color="statsColor">
|
|
||||||
[[ sizeFormat(clientStats.up) ]] /
|
|
||||||
[[ sizeFormat(clientStats.down) ]]
|
|
||||||
([[ sizeFormat(clientStats.up + clientStats.down) ]])
|
|
||||||
</a-tag>
|
|
||||||
</template>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
|
||||||
<span slot="label">
|
|
||||||
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">
|
|
||||||
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
|
|
||||||
</template>
|
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
</span>
|
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
|
||||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
|
||||||
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
|
||||||
{{end}}
|
|
|
@ -8,7 +8,7 @@
|
||||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "protocol" }}'>
|
<a-form-item label='{{ i18n "protocol" }}'>
|
||||||
<a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.protocol" style="width: 160px;">
|
||||||
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
@ -50,7 +50,6 @@
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
|
||||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
|
@ -7,14 +7,11 @@
|
||||||
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
<a-input type="number" v-model.number="inbound.settings.port"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
<a-form-item label='{{ i18n "pages.inbounds.network"}}'>
|
||||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
||||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||||
<a-select-option value="tcp">tcp</a-select-option>
|
<a-select-option value="tcp">tcp</a-select-option>
|
||||||
<a-select-option value="udp">udp</a-select-option>
|
<a-select-option value="udp">udp</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="FollowRedirect">
|
|
||||||
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
|
|
||||||
</a-form-item>
|
|
||||||
</a-form>
|
</a-form>
|
||||||
{{end}}
|
{{end}}
|
|
@ -1,7 +1,7 @@
|
||||||
{{define "form/shadowsocks"}}
|
{{define "form/shadowsocks"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "encryption" }}'>
|
<a-form-item label='{{ i18n "encryption" }}'>
|
||||||
<a-select v-model="inbound.settings.method" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.settings.method" style="width: 165px;">
|
||||||
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
@ -9,7 +9,7 @@
|
||||||
<a-input v-model.trim="inbound.settings.password"></a-input>
|
<a-input v-model.trim="inbound.settings.password"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
|
||||||
<a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.settings.network" style="width: 100px;">
|
||||||
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
<a-select-option value="tcp,udp">tcp+udp</a-select-option>
|
||||||
<a-select-option value="tcp">tcp</a-select-option>
|
<a-select-option value="tcp">tcp</a-select-option>
|
||||||
<a-select-option value="udp">udp</a-select-option>
|
<a-select-option value="udp">udp</a-select-option>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{{define "form/socks"}}
|
{{define "form/socks"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<!-- <a-form-item label="Password authentication">-->
|
<!-- <a-form-item label="密码认证">-->
|
||||||
<a-form-item label='{{ i18n "password" }}'>
|
<a-form-item label='{{ i18n "password" }}'>
|
||||||
<a-switch :checked="inbound.settings.auth === 'password'"
|
<a-switch :checked="inbound.settings.auth === 'password'"
|
||||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{{define "form/trojan"}}
|
{{define "form/trojan"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
|
<label style="color: green;">{{ i18n "clients"}}</label>
|
||||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
<a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans"
|
||||||
|
:key="`trojan-${index}`">
|
||||||
|
|
||||||
|
<a-collapse-panel :class="getHeaderStyle(trojan.email)" :header="getHeaderText(trojan.email)">
|
||||||
|
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -10,14 +14,16 @@
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The Email Must Be Completely Unique
|
The Email Must Be Completely Unique
|
||||||
</template>
|
</template>
|
||||||
<a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
|
<!--Renew Svg Icon-->
|
||||||
|
<svg
|
||||||
|
@click="getNewEmail(trojan)"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
|
<a-input v-model.trim="trojan.email" style="width: 150px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
|
||||||
<a-form-item label="Password" >
|
<a-form-item label="Password" >
|
||||||
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
|
<a-input v-model.trim="trojan.password" style="width: 150px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -29,10 +35,34 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
|
<a-input type="number" v-model.number="trojan.limitIp" min="0" style="width: 70px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item v-if="trojan.email && trojan.limitIp > 0 && isEdit">
|
||||||
|
<span slot="label">
|
||||||
|
IP log
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
clear the log
|
||||||
|
</template>
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete" @click="clearDBClientIps(trojan.email,$event)"></a-icon>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-form layout="block">
|
||||||
|
<a-textarea readonly @click="getDBClientIps(trojan.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
||||||
|
</a-textarea>
|
||||||
|
</a-form>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
<a-form-item v-if="inbound.XTLS" label="Flow">
|
<a-form-item v-if="inbound.XTLS" label="Flow">
|
||||||
<a-select v-model="client.flow" style="width: 150px">
|
<a-select v-model="trojan.flow" style="width: 150px">
|
||||||
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
<a-select-option value="">{{ i18n "none" }}</a-select-option>
|
||||||
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
|
@ -47,7 +77,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
<a-input-number v-model="trojan._totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -60,23 +90,40 @@
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
v-model="trojan._expiryTime" style="width: 170px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-tooltip v-if="trojan._totalGB > 0">
|
||||||
|
<template slot="title">
|
||||||
|
{{ i18n "pages.inbounds.resetTraffic" }}
|
||||||
|
</template>
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
|
||||||
|
<a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
|
||||||
|
<a-tag v-show="inbound.settings.trojans.length > 1" @click="removeClient(index, inbound.settings.trojans)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
|
||||||
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
|
<path fill="#EC4899"
|
||||||
|
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a-tag>
|
||||||
|
</a-form>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-tag @click="addClient(inbound.protocol, inbound.settings.trojans)">
|
||||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
|
||||||
<table width="100%">
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
<tr class="client-table-header">
|
<path fill="green"
|
||||||
<th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
|
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||||
</tr>
|
/>
|
||||||
<tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
</svg>
|
||||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
</a-tag>
|
||||||
</tr>
|
|
||||||
</table>
|
<template v-if="inbound.isTcp && inbound.tls || inbound.XTLS">
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
<template v-if="inbound.isTcp && inbound.tls">
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label="Fallbacks">
|
<a-form-item label="Fallbacks">
|
||||||
<a-row>
|
<a-row>
|
||||||
|
@ -95,19 +142,19 @@
|
||||||
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
||||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||||
</a-divider>
|
</a-divider>
|
||||||
<a-form-item label="Name">
|
<a-form-item label="name">
|
||||||
<a-input v-model="fallback.name"></a-input>
|
<a-input v-model="fallback.name"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Alpn">
|
<a-form-item label="alpn">
|
||||||
<a-input v-model="fallback.alpn"></a-input>
|
<a-input v-model="fallback.alpn"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Path">
|
<a-form-item label="path">
|
||||||
<a-input v-model="fallback.path"></a-input>
|
<a-input v-model="fallback.path"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Dest">
|
<a-form-item label="dest">
|
||||||
<a-input v-model="fallback.dest"></a-input>
|
<a-input v-model="fallback.dest"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="xVer">
|
<a-form-item label="xver">
|
||||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||||
|
|
|
@ -1,7 +1,12 @@
|
||||||
{{define "form/vless"}}
|
{{define "form/vless"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
|
<label style="color: green;">{{ i18n "clients"}}</label>
|
||||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses"
|
||||||
|
:key="`vless-${index}`">
|
||||||
|
|
||||||
|
<a-collapse-panel :class="getHeaderStyle(vless.email)" :header="getHeaderText(vless.email)">
|
||||||
|
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||||
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -10,14 +15,16 @@
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The Email Must Be Completely Unique
|
The Email Must Be Completely Unique
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
<!--Renew Svg Icon-->
|
||||||
|
<svg
|
||||||
|
@click="getNewEmail(vless)"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
|
<a-input v-model.trim="vless.email" style="width: 150px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
|
||||||
<a-form-item label="ID">
|
<a-form-item label="ID">
|
||||||
<a-input v-model.trim="client.id" style="width: 300px;" ></a-input>
|
<a-input v-model.trim="vless.id" style="width: 300px;" ></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -29,8 +36,33 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
|
<a-input type="number" v-model.number="vless.limitIp" min="0" style="width: 70px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit">
|
||||||
|
<span slot="label">
|
||||||
|
IP log
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
clear the log
|
||||||
|
</template>
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete" @click="clearDBClientIps(vless.email,$event)"></a-icon>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-form layout="block">
|
||||||
|
|
||||||
|
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
||||||
|
</a-textarea>
|
||||||
|
</a-form>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
<a-form-item v-if="inbound.XTLS" label="Flow">
|
<a-form-item v-if="inbound.XTLS" label="Flow">
|
||||||
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
|
||||||
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
|
||||||
|
@ -53,7 +85,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
<a-input-number v-model="vless._totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -66,23 +98,41 @@
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-tooltip v-if="vless._totalGB > 0">
|
||||||
|
<template slot="title">
|
||||||
|
{{ i18n "pages.inbounds.resetTraffic" }}
|
||||||
|
</template>
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
|
||||||
|
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
|
||||||
|
|
||||||
|
<a-tag v-show="inbound.settings.vlesses.length > 1" @click="removeClient(index, inbound.settings.vlesses)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
|
||||||
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
|
<path fill="#EC4899"
|
||||||
|
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a-tag>
|
||||||
|
</a-form>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
<a-tag @click="addClient(inbound.protocol, inbound.settings.vlesses)">
|
||||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
|
||||||
<table width="100%">
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
<tr class="client-table-header">
|
<path fill="green"
|
||||||
<th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
|
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||||
</tr>
|
/>
|
||||||
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
</svg>
|
||||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
</a-tag>
|
||||||
</tr>
|
|
||||||
</table>
|
<template v-if="inbound.isTcp && inbound.tls || inbound.XTLS">
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
<template v-if="inbound.isTcp && inbound.tls">
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label="Fallbacks">
|
<a-form-item label="Fallbacks">
|
||||||
<a-row>
|
<a-row>
|
||||||
|
@ -101,19 +151,19 @@
|
||||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||||
</a-divider>
|
</a-divider>
|
||||||
<a-form-item label="Name">
|
<a-form-item label="name">
|
||||||
<a-input v-model="fallback.name"></a-input>
|
<a-input v-model="fallback.name"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Alpn">
|
<a-form-item label="alpn">
|
||||||
<a-input v-model="fallback.alpn"></a-input>
|
<a-input v-model="fallback.alpn"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Path">
|
<a-form-item label="path">
|
||||||
<a-input v-model="fallback.path"></a-input>
|
<a-input v-model="fallback.path"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Dest">
|
<a-form-item label="dest">
|
||||||
<a-input v-model="fallback.dest"></a-input>
|
<a-input v-model="fallback.dest"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="xVer">
|
<a-form-item label="xver">
|
||||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
{{define "form/vmess"}}
|
{{define "form/vmess"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
|
<label style="color: green;">{{ i18n "clients"}}</label>
|
||||||
<a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
|
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses"
|
||||||
|
:key="`vmess-${index}`">
|
||||||
|
<a-collapse-panel :class="getHeaderStyle(vmess.email)" :header="getHeaderText(vmess.email)">
|
||||||
|
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
|
||||||
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -10,30 +14,54 @@
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
The Email Must Be Completely Unique
|
The Email Must Be Completely Unique
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
|
<!--Renew Svg Icon-->
|
||||||
|
<svg
|
||||||
|
@click="getNewEmail(vmess)"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input v-model.trim="client.email" style="width: 150px;"></a-input>
|
<a-input v-model.trim="vmess.email" style="width: 150px;"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
|
||||||
<a-form-item label="ID">
|
<a-form-item label="ID">
|
||||||
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
|
<a-input v-model.trim="vmess.id" style="width: 300px;" ></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "additional" }} ID'>
|
<a-form-item label='{{ i18n "additional" }} ID'>
|
||||||
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
|
<a-input type="number" v-model.number="vmess.alterId"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
IP Count Limit
|
IP Count Limit
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
Disable inbound if more than entered count (0 for disable limit ip)
|
disable inbound if more than entered count (0 for disable limit ip)
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;"></a-input>
|
<a-input type="number" v-model.number="vmess.limitIp" min="0" style="width: 70px;" ></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
|
||||||
|
<span slot="label">
|
||||||
|
IP Log
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
|
||||||
|
</template>
|
||||||
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
clear the log
|
||||||
|
</template>
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete" @click="clearDBClientIps(vmess.email,$event)"></a-icon>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
</span>
|
||||||
|
<a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
|
||||||
|
</a-textarea>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
|
||||||
|
@ -44,7 +72,7 @@
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-input-number v-model="client._totalGB" :min="0"></a-input-number>
|
<a-input-number v-model="vmess._totalGB" :min="0"></a-input-number>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<span slot="label">
|
<span slot="label">
|
||||||
|
@ -57,22 +85,43 @@
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</span>
|
</span>
|
||||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||||
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
|
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form layout="inline">
|
||||||
|
<a-tooltip v-if="vmess._totalGB > 0">
|
||||||
|
<template slot="title">
|
||||||
|
{{ i18n "pages.inbounds.resetTraffic" }}
|
||||||
|
</template>
|
||||||
|
<span style="color: #FF4D4F">
|
||||||
|
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
|
||||||
|
</span>
|
||||||
|
</a-tooltip>
|
||||||
|
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
|
||||||
|
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
|
||||||
|
<a-tag v-show="inbound.settings.vmesses.length > 1" @click="removeClient(index, inbound.settings.vmesses)">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
|
||||||
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
|
<path fill="#EC4899"
|
||||||
|
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</a-tag>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
|
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<a-collapse v-else>
|
|
||||||
<a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
|
<a-tag @click="addClient(inbound.protocol, inbound.settings.vmesses)">
|
||||||
<table width="100%">
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer">
|
||||||
<tr class="client-table-header">
|
<path fill="none" d="M0 0h24v24H0z" />
|
||||||
<th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
|
<path fill="green"
|
||||||
</tr>
|
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z"
|
||||||
<tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
|
/>
|
||||||
<td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
|
</svg>
|
||||||
</tr>
|
</a-tag>
|
||||||
</table>
|
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
|
||||||
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
{{define "form/streamQUIC"}}
|
{{define "form/streamQUIC"}}
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||||
<a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.stream.quic.security" style="width: 165px;">
|
||||||
<a-select-option value="none">none</a-select-option>
|
<a-select-option value="none">none</a-select-option>
|
||||||
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
|
||||||
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||||
<a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.stream.quic.type" style="width: 280px;">
|
||||||
<a-select-option value="none">none(not camouflage)</a-select-option>
|
<a-select-option value="none">none(not camouflage)</a-select-option>
|
||||||
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
<a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
|
||||||
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
<a-select-option value="utp">utp(camouflage BT download)</a-select-option>
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
<!-- select stream network -->
|
<!-- select stream network -->
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item label='{{ i18n "transmission" }}'>
|
<a-form-item label='{{ i18n "transmission" }}'>
|
||||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.stream.network" @change="streamNetworkChange">
|
||||||
<a-select-option value="tcp">TCP</a-select-option>
|
<a-select-option value="tcp">TCP</a-select-option>
|
||||||
<a-select-option value="kcp">KCP</a-select-option>
|
<a-select-option value="kcp">KCP</a-select-option>
|
||||||
<a-select-option value="ws">WS</a-select-option>
|
<a-select-option value="ws">WS</a-select-option>
|
||||||
<a-select-option value="http">HTTP</a-select-option>
|
<a-select-option value="http">HTTP</a-select-option>
|
||||||
<a-select-option value="quic">QUIC</a-select-option>
|
<a-select-option value="quic">QUIC</a-select-option>
|
||||||
<a-select-option value="grpc">GRPC</a-select-option>
|
<a-select-option value="grpc">gRPC</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
|
|
|
@ -22,12 +22,12 @@
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="MinVersion">
|
<a-form-item label="MinVersion">
|
||||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px">
|
||||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="MaxVersion">
|
<a-form-item label="MaxVersion">
|
||||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
|
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px">
|
||||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
@ -40,11 +40,13 @@
|
||||||
<a-form-item label='{{ i18n "domainName" }}'>
|
<a-form-item label='{{ i18n "domainName" }}'>
|
||||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="Alpn" v-if="inbound.tls">
|
<a-form-item label="Alpn">
|
||||||
<a-select v-model="inbound.stream.tls.alpn[0]" style="width:200px">
|
<a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
|
||||||
<a-select-option value=''>auto</a-select-option>
|
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
|
||||||
<a-select-option v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-select-option>
|
</a-checkbox-group>
|
||||||
</a-select>
|
</a-form-item>
|
||||||
|
<a-form-item label="Allow insecure">
|
||||||
|
<a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='{{ i18n "certificate" }}'>
|
<a-form-item label='{{ i18n "certificate" }}'>
|
||||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
|
||||||
|
|
|
@ -44,7 +44,7 @@
|
||||||
</template>
|
</template>
|
||||||
</table>
|
</table>
|
||||||
</td></tr>
|
</td></tr>
|
||||||
<tr colspan="2" v-if="dbInbound.hasLink()">
|
<tr colspan="2">
|
||||||
<td v-if="inbound.tls">
|
<td v-if="inbound.tls">
|
||||||
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
|
||||||
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||||
|
@ -57,20 +57,20 @@
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<template v-if="infoModal.clientSettings">
|
|
||||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||||
<table style="margin-bottom: 10px; width: 100%;">
|
<table style="margin-bottom: 10px; width: 100%;">
|
||||||
|
<tr><th>[[ Object.keys(infoModal.clientSettings)[0] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[1] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[2] ]]</th></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th v-for="col in Object.keys(infoModal.clientSettings).slice(0, 3)">[[ col ]]</th>
|
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[0] ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[1] ]]</a-tag></td>
|
||||||
|
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[2] ]]</a-tag></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
|
||||||
<td v-for="col in Object.values(infoModal.clientSettings).slice(0, 3)"><a-tag color="green">[[ col ]]</a-tag></td>
|
|
||||||
</table>
|
</table>
|
||||||
<table style="margin-bottom: 10px; width: 100%;">
|
<table style="margin-bottom: 10px; width: 100%;">
|
||||||
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
|
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
|
<a-tag :color="statsColor(infoModal.clientStats)">
|
||||||
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
[[ sizeFormat(infoModal.clientStats['up']) ]] /
|
||||||
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
[[ sizeFormat(infoModal.clientStats['down']) ]]
|
||||||
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
|
||||||
|
@ -89,72 +89,11 @@
|
||||||
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
|
<a-tag v-if="infoModal.clientStats.enable" color="blue">{{ i18n "enabled" }}</a-tag>
|
||||||
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
|
||||||
<template v-else>
|
|
||||||
<a-divider></a-divider>
|
|
||||||
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<th>{{ i18n "encryption" }}</th>
|
|
||||||
<th>{{ i18n "password" }}</th>
|
|
||||||
<th>{{ i18n "pages.inbounds.network" }}</th>
|
|
||||||
</tr><tr>
|
|
||||||
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
|
|
||||||
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
|
|
||||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
|
||||||
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
|
||||||
<th>{{ i18n "pages.inbounds.network" }}</th>
|
|
||||||
<th>FollowRedirect</th>
|
|
||||||
</tr><tr>
|
|
||||||
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
|
|
||||||
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
|
|
||||||
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
|
|
||||||
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</table>
|
|
||||||
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<th>{{ i18n "password" }} Auth</th>
|
|
||||||
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
|
||||||
<th>IP</th>
|
|
||||||
</tr><tr>
|
|
||||||
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
|
|
||||||
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
|
|
||||||
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
|
|
||||||
</tr><tr v-if="inbound.settings.auth == 'password'">
|
|
||||||
<td> </td>
|
|
||||||
<td>{{ i18n "username" }}</td>
|
|
||||||
<td>{{ i18n "password" }}</td>
|
|
||||||
</tr><tr v-for="account,index in inbound.settings.accounts">
|
|
||||||
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
|
||||||
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
|
||||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</table>
|
|
||||||
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
|
|
||||||
<tr>
|
|
||||||
<th> </th>
|
|
||||||
<th>{{ i18n "username" }}</th>
|
|
||||||
<th>{{ i18n "password" }}</th>
|
|
||||||
</tr><tr v-for="account,index in inbound.settings.accounts">
|
|
||||||
<td><a-tag color="green">[[ index ]]</a-tag></td>
|
|
||||||
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
|
|
||||||
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</table>
|
|
||||||
</template>
|
|
||||||
<div v-if="dbInbound.hasLink()">
|
<div v-if="dbInbound.hasLink()">
|
||||||
<a-divider>URL</a-divider>
|
<a-divider>URL</a-divider>
|
||||||
<p>[[ infoModal.link ]]</p>
|
<p>[[ infoModal.link ]]</p>
|
||||||
|
@ -167,31 +106,39 @@
|
||||||
visible: false,
|
visible: false,
|
||||||
inbound: new Inbound(),
|
inbound: new Inbound(),
|
||||||
dbInbound: new DBInbound(),
|
dbInbound: new DBInbound(),
|
||||||
settings: null,
|
|
||||||
clientSettings: new Inbound.Settings(),
|
clientSettings: new Inbound.Settings(),
|
||||||
clientStats: [],
|
clientStats: [],
|
||||||
upStats: 0,
|
upStats: 0,
|
||||||
downStats: 0,
|
downStats: 0,
|
||||||
clipboard: null,
|
clipboard: null,
|
||||||
link: null,
|
link: null,
|
||||||
index: null,
|
index: 0,
|
||||||
isExpired: false,
|
isExpired: false,
|
||||||
show(dbInbound, index) {
|
show(dbInbound, index=0) {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
this.inbound = dbInbound.toInbound();
|
this.inbound = dbInbound.toInbound();
|
||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.link = dbInbound.genLink(index);
|
this.link = dbInbound.genLink(index);
|
||||||
this.settings = JSON.parse(this.inbound.settings);
|
this.clientSettings = Object.values(JSON.parse(this.inbound.settings).clients)[index];
|
||||||
this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
|
this.clientStats = dbInbound.clientStats;
|
||||||
this.isExpired = this.inbound.isExpiry(index);
|
this.isExpired = this.inbound.isExpiry(index);
|
||||||
this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
if(dbInbound.clientStats.length > 0)
|
||||||
|
{
|
||||||
|
for (const key in dbInbound.clientStats) {
|
||||||
|
if (Object.hasOwnProperty.call(dbInbound.clientStats, key)) {
|
||||||
|
if(dbInbound.clientStats[key]['email'] == this.clientSettings.email)
|
||||||
|
this.clientStats = dbInbound.clientStats[key];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
infoModalApp.$nextTick(() => {
|
infoModalApp.$nextTick(() => {
|
||||||
if (this.clipboard === null) {
|
if (this.clipboard === null) {
|
||||||
this.clipboard = new ClipboardJS('#copy-url-link', {
|
this.clipboard = new ClipboardJS('#copy-url-link', {
|
||||||
text: () => this.link,
|
text: () => this.link,
|
||||||
});
|
});
|
||||||
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
|
this.clipboard.on('success', () => app.$message.success('{{ i18n "copySuccess" }}'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -199,7 +146,6 @@
|
||||||
infoModal.visible = false;
|
infoModal.visible = false;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const infoModalApp = new Vue({
|
const infoModalApp = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#inbound-info-modal',
|
el: '#inbound-info-modal',
|
||||||
|
@ -210,33 +156,32 @@
|
||||||
},
|
},
|
||||||
get inbound() {
|
get inbound() {
|
||||||
return this.infoModal.inbound;
|
return this.infoModal.inbound;
|
||||||
},
|
|
||||||
get isEnable() {
|
|
||||||
if(infoModal.clientStats){
|
|
||||||
return infoModal.clientStats.enable;
|
|
||||||
}
|
|
||||||
return infoModal.dbInbound.isEnable;
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
setQrCode(elmentId,index) {
|
||||||
|
content = infoModal.inbound.genLink(infoModal.dbInbound.address,infoModal.dbInbound.remark,index)
|
||||||
|
new QRious({
|
||||||
|
element: document.querySelector('#'+elmentId),
|
||||||
|
size: 260,
|
||||||
|
value: content,
|
||||||
|
});
|
||||||
|
},
|
||||||
copyTextToClipboard(elmentId,content) {
|
copyTextToClipboard(elmentId,content) {
|
||||||
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
|
||||||
text: () => content,
|
text: () => content,
|
||||||
});
|
});
|
||||||
this.infoModal.clipboard.on('success', () => {
|
this.infoModal.clipboard.on('success', () => {
|
||||||
app.$message.success('{{ i18n "copied" }}')
|
app.$message.success('{{ i18n "copySuccess" }}')
|
||||||
this.infoModal.clipboard.destroy();
|
this.infoModal.clipboard.destroy();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
statsColor(stats) {
|
statsColor(stats) {
|
||||||
if(!stats) return 'blue'
|
|
||||||
if(stats['total'] === 0) return 'blue'
|
if(stats['total'] === 0) return 'blue'
|
||||||
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
|
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
|
||||||
else return 'red'
|
else return 'red'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
{{end}}
|
{{end}}
|
|
@ -89,21 +89,93 @@
|
||||||
removeClient(index, clients) {
|
removeClient(index, clients) {
|
||||||
clients.splice(index, 1);
|
clients.splice(index, 1);
|
||||||
},
|
},
|
||||||
|
async getDBClientIps(email, event) {
|
||||||
|
const msg = await HttpUtil.post('/xui/inbound/clientIps/' + email);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
let ips = JSON.parse(msg.obj);
|
||||||
|
ips = ips.join(",");
|
||||||
|
event.target.value = ips;
|
||||||
|
} catch (error) {
|
||||||
|
event.target.value = msg.obj;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async clearDBClientIps(email,event) {
|
||||||
|
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.target.value = ""
|
||||||
|
},
|
||||||
|
async resetClientTraffic(client, event) {
|
||||||
|
const msg = await HttpUtil.post(`/xui/inbound/resetClientTraffic/${client.email}`);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const clientStats = this.inbound.clientStats;
|
||||||
|
if (clientStats.length > 0) {
|
||||||
|
for (let i = 0; i < clientStats.length; i++) {
|
||||||
|
if (clientStats[i].email === client.email) {
|
||||||
|
clientStats[i].up = 0;
|
||||||
|
clientStats[i].down = 0;
|
||||||
|
break; // Stop looping once we've found the matching client.
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
isExpiry(index) {
|
isExpiry(index) {
|
||||||
return this.inbound.isExpiry(index)
|
return this.inbound.isExpiry(index)
|
||||||
},
|
},
|
||||||
|
getUpStats(email) {
|
||||||
|
clientStats = this.inbound.clientStats
|
||||||
|
if(clientStats.length > 0)
|
||||||
|
{
|
||||||
|
for (const key in clientStats) {
|
||||||
|
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||||
|
if(clientStats[key]['email'] == email)
|
||||||
|
return clientStats[key]['up']
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getDownStats(email) {
|
||||||
|
clientStats = this.inbound.clientStats
|
||||||
|
if(clientStats.length > 0)
|
||||||
|
{
|
||||||
|
for (const key in clientStats) {
|
||||||
|
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||||
|
if(clientStats[key]['email'] == email)
|
||||||
|
return clientStats[key]['down']
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
isClientEnable(email) {
|
isClientEnable(email) {
|
||||||
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
|
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||||
return clientStats ? clientStats['enable'] : true
|
return clientStats ? clientStats['enable'] : true
|
||||||
},
|
},
|
||||||
|
getHeaderText(email) {
|
||||||
|
if(email == "")
|
||||||
|
return "Add Client"
|
||||||
|
|
||||||
|
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
|
||||||
|
},
|
||||||
|
getHeaderStyle(email) {
|
||||||
|
return (this.isClientEnable(email) == true ? '' : 'deactive-client')
|
||||||
|
},
|
||||||
getNewEmail(client) {
|
getNewEmail(client) {
|
||||||
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
|
||||||
var string = '';
|
var string = '';
|
||||||
var len = 6 + Math.floor(Math.random() * 5);
|
var len = 7 + Math.floor(Math.random() * 5)
|
||||||
for(var ii=0; ii<len; ii++){
|
for(var ii=0; ii<len; ii++){
|
||||||
string += chars[Math.floor(Math.random() * chars.length)];
|
string += chars[Math.floor(Math.random() * chars.length)];
|
||||||
}
|
}
|
||||||
client.email = string;
|
client.email = string
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -11,10 +11,6 @@
|
||||||
.ant-col-sm-24 {
|
.ant-col-sm-24 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-table-row-expand-icon {
|
|
||||||
color: rgba(0,0,0,.65);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<a-layout id="app" v-cloak>
|
<a-layout id="app" v-cloak>
|
||||||
|
@ -31,15 +27,15 @@
|
||||||
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
<a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "pages.inbounds.totalDownUp" }}:
|
{{ i18n "pages.inbounds.totalDownUp" }}:
|
||||||
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "pages.inbounds.totalUsage" }}:
|
{{ i18n "pages.inbounds.totalUsage" }}:
|
||||||
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
{{ i18n "pages.inbounds.inboundCount" }}:
|
{{ i18n "pages.inbounds.inboundCount" }}:
|
||||||
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="24" :lg="12">
|
<a-col :xs="24" :sm="24" :lg="12">
|
||||||
|
@ -54,8 +50,8 @@
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
<div slot="title">
|
<div slot="title">
|
||||||
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
|
<a-button type="primary" @click="openAddInbound">Add Inbound</a-button>
|
||||||
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
|
<a-button type="primary" @click="exportAllLinks" class="copy-btn">Export Links</a-button>
|
||||||
</div>
|
</div>
|
||||||
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
|
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
|
||||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||||
|
@ -68,7 +64,7 @@
|
||||||
<a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
|
<a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
|
||||||
<a-dropdown :trigger="['click']">
|
<a-dropdown :trigger="['click']">
|
||||||
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
|
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
|
||||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme" style="border: 1px solid rgba(255, 255, 255, 0.65);">
|
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
|
||||||
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
|
<a-menu-item v-if="dbInbound.isSS" key="qrcode">
|
||||||
<a-icon type="qrcode"></a-icon>
|
<a-icon type="qrcode"></a-icon>
|
||||||
{{ i18n "qrCode" }}
|
{{ i18n "qrCode" }}
|
||||||
|
@ -78,25 +74,11 @@
|
||||||
{{ i18n "edit" }}
|
{{ i18n "edit" }}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
|
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
|
||||||
<a-menu-item key="addClient">
|
|
||||||
<a-icon type="user"></a-icon>
|
|
||||||
{{ i18n "pages.client.add"}}
|
|
||||||
</a-menu-item>
|
|
||||||
<a-menu-item key="addBulkClient">
|
|
||||||
<a-icon type="team"></a-icon>
|
|
||||||
{{ i18n "pages.client.bulk"}}
|
|
||||||
</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>
|
||||||
{{ i18n "pages.inbounds.export"}}
|
{{ i18n "pages.inbounds.export"}}
|
||||||
</a-menu-item>
|
</a-menu-item>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
|
||||||
<a-menu-item key="showInfo">
|
|
||||||
<a-icon type="info-circle"></a-icon>
|
|
||||||
{{ i18n "info"}}
|
|
||||||
</a-menu-item>
|
|
||||||
</template>
|
|
||||||
<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>
|
||||||
|
@ -128,7 +110,7 @@
|
||||||
<template v-else>{{ i18n "none" }}</template>
|
<template v-else>{{ i18n "none" }}</template>
|
||||||
</template>
|
</template>
|
||||||
<template slot="enable" slot-scope="text, dbInbound">
|
<template slot="enable" slot-scope="text, dbInbound">
|
||||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
|
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||||
<template v-if="dbInbound.expiryTime > 0">
|
<template v-if="dbInbound.expiryTime > 0">
|
||||||
|
@ -149,7 +131,7 @@
|
||||||
:data-source="getInboundClients(record)"
|
:data-source="getInboundClients(record)"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
>
|
>
|
||||||
{{template "client_table"}}
|
{{template "client_row"}}
|
||||||
</a-table>
|
</a-table>
|
||||||
<a-table
|
<a-table
|
||||||
v-else-if="record.protocol === Protocols.TROJAN"
|
v-else-if="record.protocol === Protocols.TROJAN"
|
||||||
|
@ -158,7 +140,16 @@
|
||||||
:data-source="getInboundClients(record)"
|
:data-source="getInboundClients(record)"
|
||||||
:pagination="false"
|
:pagination="false"
|
||||||
>
|
>
|
||||||
{{template "client_table"}}
|
{{template "client_row"}}
|
||||||
|
</a-table>
|
||||||
|
<a-table
|
||||||
|
v-else
|
||||||
|
:row-key="client => client.id"
|
||||||
|
:columns="innerOneColumns"
|
||||||
|
:data-source="record"
|
||||||
|
:pagination="false"
|
||||||
|
>
|
||||||
|
{{template "client_row"}}
|
||||||
</a-table>
|
</a-table>
|
||||||
</template>
|
</template>
|
||||||
</a-table>
|
</a-table>
|
||||||
|
@ -182,7 +173,7 @@
|
||||||
width: 40,
|
width: 40,
|
||||||
scopedSlots: { customRender: 'enable' },
|
scopedSlots: { customRender: 'enable' },
|
||||||
}, {
|
}, {
|
||||||
title: "ID",
|
title: "Id",
|
||||||
align: 'center',
|
align: 'center',
|
||||||
dataIndex: "id",
|
dataIndex: "id",
|
||||||
width: 30,
|
width: 30,
|
||||||
|
@ -219,20 +210,26 @@
|
||||||
}];
|
}];
|
||||||
|
|
||||||
const innerColumns = [
|
const innerColumns = [
|
||||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: 'UID', width: 150, dataIndex: "id" },
|
{ title: 'UID', width: 150, dataIndex: "id" },
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const innerTrojanColumns = [
|
const innerTrojanColumns = [
|
||||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
|
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: 'Password', width: 100, dataIndex: "password" },
|
{ title: 'Password', width: 100, dataIndex: "password" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const innerOneColumns = [
|
||||||
|
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } },
|
||||||
|
];
|
||||||
|
|
||||||
const app = new Vue({
|
const app = new Vue({
|
||||||
delimiters: ['[[', ']]'],
|
delimiters: ['[[', ']]'],
|
||||||
el: '#app',
|
el: '#app',
|
||||||
|
@ -256,7 +253,6 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setInbounds(msg.obj);
|
this.setInbounds(msg.obj);
|
||||||
this.searchKey = '';
|
|
||||||
},
|
},
|
||||||
setInbounds(dbInbounds) {
|
setInbounds(dbInbounds) {
|
||||||
this.inbounds.splice(0);
|
this.inbounds.splice(0);
|
||||||
|
@ -297,26 +293,17 @@
|
||||||
case "qrcode":
|
case "qrcode":
|
||||||
this.showQrcode(dbInbound);
|
this.showQrcode(dbInbound);
|
||||||
break;
|
break;
|
||||||
case "showInfo":
|
case "export":
|
||||||
this.showInfo(dbInbound);
|
this.inboundLinks(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
case "edit":
|
case "edit":
|
||||||
this.openEditInbound(dbInbound.id);
|
this.openEditInbound(dbInbound.id);
|
||||||
break;
|
break;
|
||||||
case "addClient":
|
|
||||||
this.openAddClient(dbInbound.id)
|
|
||||||
break;
|
|
||||||
case "addBulkClient":
|
|
||||||
this.openAddBulkClient(dbInbound.id)
|
|
||||||
break;
|
|
||||||
case "export":
|
|
||||||
this.inboundLinks(dbInbound.id);
|
|
||||||
break;
|
|
||||||
case "resetTraffic":
|
case "resetTraffic":
|
||||||
this.resetTraffic(dbInbound.id);
|
this.resetTraffic(dbInbound);
|
||||||
break;
|
break;
|
||||||
case "delete":
|
case "delete":
|
||||||
this.delInbound(dbInbound.id);
|
this.delInbound(dbInbound);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -333,8 +320,8 @@
|
||||||
isEdit: false
|
isEdit: false
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
openEditInbound(dbInboundId) {
|
openEditInbound(dbInbound_id) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInbound_id);
|
||||||
const inbound = dbInbound.toInbound();
|
const inbound = dbInbound.toInbound();
|
||||||
inModal.show({
|
inModal.show({
|
||||||
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
title: '{{ i18n "pages.inbounds.modifyInbound"}}',
|
||||||
|
@ -363,10 +350,9 @@
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
protocol: inbound.protocol,
|
protocol: inbound.protocol,
|
||||||
settings: inbound.settings.toString(),
|
settings: inbound.settings.toString(),
|
||||||
|
streamSettings: inbound.stream.toString(),
|
||||||
|
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
|
||||||
};
|
};
|
||||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
|
||||||
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
|
||||||
|
|
||||||
await this.submit('/xui/inbound/add', data, inModal);
|
await this.submit('/xui/inbound/add', data, inModal);
|
||||||
},
|
},
|
||||||
async updateInbound(inbound, dbInbound) {
|
async updateInbound(inbound, dbInbound) {
|
||||||
|
@ -382,80 +368,15 @@
|
||||||
port: inbound.port,
|
port: inbound.port,
|
||||||
protocol: inbound.protocol,
|
protocol: inbound.protocol,
|
||||||
settings: inbound.settings.toString(),
|
settings: inbound.settings.toString(),
|
||||||
|
streamSettings: inbound.stream.toString(),
|
||||||
|
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
|
||||||
};
|
};
|
||||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
|
||||||
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
|
||||||
|
|
||||||
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
|
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
|
||||||
},
|
},
|
||||||
openAddClient(dbInboundId) {
|
resetTraffic(dbInbound) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
|
||||||
clientModal.show({
|
|
||||||
title: '{{ i18n "pages.client.add"}}',
|
|
||||||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
|
||||||
dbInbound: dbInbound,
|
|
||||||
confirm: async (inbound, dbInbound, index) => {
|
|
||||||
clientModal.loading();
|
|
||||||
await this.addClient(inbound, dbInbound);
|
|
||||||
clientModal.close();
|
|
||||||
},
|
|
||||||
isEdit: false
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openAddBulkClient(dbInboundId) {
|
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
|
||||||
clientsBulkModal.show({
|
|
||||||
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
|
|
||||||
okText: '{{ i18n "pages.client.bulk"}}',
|
|
||||||
dbInbound: dbInbound,
|
|
||||||
confirm: async (inbound, dbInbound) => {
|
|
||||||
clientsBulkModal.loading();
|
|
||||||
await this.addClient(inbound, dbInbound);
|
|
||||||
clientsBulkModal.close();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
openEditClient(dbInboundId, client) {
|
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
|
||||||
clients = this.getInboundClients(dbInbound);
|
|
||||||
index = this.findIndexOfClient(clients, client);
|
|
||||||
clientModal.show({
|
|
||||||
title: '{{ i18n "pages.client.edit"}}',
|
|
||||||
okText: '{{ i18n "pages.client.submitEdit"}}',
|
|
||||||
dbInbound: dbInbound,
|
|
||||||
index: index,
|
|
||||||
confirm: async (inbound, dbInbound, index) => {
|
|
||||||
clientModal.loading();
|
|
||||||
await this.updateClient(inbound, dbInbound, index);
|
|
||||||
clientModal.close();
|
|
||||||
},
|
|
||||||
isEdit: true
|
|
||||||
});
|
|
||||||
},
|
|
||||||
findIndexOfClient(clients,client) {
|
|
||||||
firstKey = Object.keys(client)[0];
|
|
||||||
return clients.findIndex(c => c[firstKey] === client[firstKey]);
|
|
||||||
},
|
|
||||||
async addClient(inbound, dbInbound) {
|
|
||||||
const data = {
|
|
||||||
id: dbInbound.id,
|
|
||||||
settings: inbound.settings.toString(),
|
|
||||||
};
|
|
||||||
await this.submit('/xui/inbound/addClient', data);
|
|
||||||
},
|
|
||||||
async updateClient(inbound, dbInbound, index) {
|
|
||||||
const data = {
|
|
||||||
id: dbInbound.id,
|
|
||||||
settings: inbound.settings.toString(),
|
|
||||||
};
|
|
||||||
await this.submit(`/xui/inbound/updateClient/${index}`, data);
|
|
||||||
},
|
|
||||||
resetTraffic(dbInboundId) {
|
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => {
|
onOk: () => {
|
||||||
|
@ -466,34 +387,13 @@
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
delInbound(dbInboundId) {
|
delInbound(dbInbound) {
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
|
||||||
okText: '{{ i18n "delete"}}',
|
okText: '{{ i18n "delete"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
|
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
|
||||||
});
|
|
||||||
},
|
|
||||||
delClient(dbInboundId,client) {
|
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
|
||||||
newDbInbound = new DBInbound(dbInbound);
|
|
||||||
inbound = newDbInbound.toInbound();
|
|
||||||
clients = this.getClients(dbInbound.protocol, inbound.settings);
|
|
||||||
index = this.findIndexOfClient(clients, client);
|
|
||||||
clients.splice(index, 1);
|
|
||||||
const data = {
|
|
||||||
id: dbInboundId,
|
|
||||||
settings: inbound.settings.toString(),
|
|
||||||
};
|
|
||||||
this.$confirm({
|
|
||||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
|
||||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
|
||||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
|
||||||
okText: '{{ i18n "delete"}}',
|
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
|
||||||
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getClients(protocol, clientSettings) {
|
getClients(protocol, clientSettings) {
|
||||||
|
@ -511,12 +411,11 @@
|
||||||
showInfo(dbInbound, index) {
|
showInfo(dbInbound, index) {
|
||||||
infoModal.show(dbInbound, index);
|
infoModal.show(dbInbound, index);
|
||||||
},
|
},
|
||||||
switchEnable(dbInboundId) {
|
switchEnable(dbInbound) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
|
||||||
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
|
|
||||||
},
|
},
|
||||||
async submit(url, data) {
|
async submit(url, data, modal) {
|
||||||
const msg = await HttpUtil.postWithModal(url, data);
|
const msg = await HttpUtil.postWithModal(url, data, modal);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
await this.getDBInbounds();
|
await this.getDBInbounds();
|
||||||
}
|
}
|
||||||
|
@ -530,15 +429,34 @@
|
||||||
return dbInbound.toInbound().settings.trojans
|
return dbInbound.toInbound().settings.trojans
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetClientTraffic(client,dbInboundId) {
|
resetClientTraffic(client,inbound,event) {
|
||||||
this.$confirm({
|
this.$confirm({
|
||||||
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
|
||||||
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
|
||||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
|
onOk: () => {
|
||||||
})
|
this.resetClTraffic(client,inbound,event);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
},
|
||||||
|
async resetClTraffic(client,inbound,event) {
|
||||||
|
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clientStats = inbound.clientStats
|
||||||
|
if(clientStats.length > 0)
|
||||||
|
{
|
||||||
|
for (const key in clientStats) {
|
||||||
|
if (Object.hasOwnProperty.call(clientStats, key)) {
|
||||||
|
if(clientStats[key]['email'] == client.email){
|
||||||
|
clientStats[key]['up'] = 0
|
||||||
|
clientStats[key]['down'] = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
isExpiry(dbInbound, index) {
|
isExpiry(dbInbound, index) {
|
||||||
return dbInbound.toInbound().isExpiry(index)
|
return dbInbound.toInbound().isExpiry(index)
|
||||||
|
@ -558,13 +476,6 @@
|
||||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
clientStats = dbInbound.clientStats.find(stats => stats.email === email)
|
||||||
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
|
return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
|
||||||
},
|
},
|
||||||
isClientEnabled(dbInbound, email) {
|
|
||||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
|
||||||
return clientStats ? clientStats['enable'] : true
|
|
||||||
},
|
|
||||||
isRemovable(dbInbound_id){
|
|
||||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
|
|
||||||
},
|
|
||||||
inboundLinks(dbInboundId) {
|
inboundLinks(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
|
txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
|
||||||
|
@ -576,6 +487,10 @@
|
||||||
}
|
}
|
||||||
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
|
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
|
||||||
},
|
},
|
||||||
|
isClientEnabled(dbInbound, email) {
|
||||||
|
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
|
||||||
|
return clientStats ? clientStats['enable'] : true
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
searchKey: debounce(function (newVal) {
|
searchKey: debounce(function (newVal) {
|
||||||
|
@ -626,7 +541,5 @@
|
||||||
{{template "qrcodeModal"}}
|
{{template "qrcodeModal"}}
|
||||||
{{template "textModal"}}
|
{{template "textModal"}}
|
||||||
{{template "inboundInfoModal"}}
|
{{template "inboundInfoModal"}}
|
||||||
{{template "clientsModal"}}
|
|
||||||
{{template "clientsBulkModal"}}
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
|
@ -1,24 +1,16 @@
|
||||||
{{define "client_table"}}
|
{{define "client_row"}}
|
||||||
<template slot="actions" slot-scope="text, client, index">
|
<template slot="actions" slot-scope="text, client, index">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "qrCode" }}</template>
|
<template slot="title">{{ i18n "qrCode" }}</template>
|
||||||
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
|
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title">{{ i18n "pages.client.edit" }}</template>
|
|
||||||
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
|
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "info" }}</template>
|
<template slot="title">{{ i18n "info" }}</template>
|
||||||
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
|
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
|
||||||
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
|
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record,$event)" v-if="client.email != ''"></a-icon>
|
||||||
</a-tooltip>
|
|
||||||
<a-tooltip>
|
|
||||||
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
|
|
||||||
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
|
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<template slot="client" slot-scope="text, client">
|
<template slot="client" slot-scope="text, client">
|
|
@ -11,10 +11,6 @@
|
||||||
.ant-col-sm-24 {
|
.ant-col-sm-24 {
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.ant-card-dark h2 {
|
|
||||||
color: hsla(0,0%,100%,.65);
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
<body>
|
<body>
|
||||||
<a-layout id="app" v-cloak>
|
<a-layout id="app" v-cloak>
|
||||||
|
@ -84,16 +80,16 @@
|
||||||
</template>
|
</template>
|
||||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
<a-tag color="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
<a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
|
||||||
<a-tag color="blue" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
<a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
|
||||||
<a-tag color="blue" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||||
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
|
||||||
{{ i18n "pages.index.operationHours" }}:
|
{{ i18n "pages.index.operationHours" }}:
|
||||||
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag>
|
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
{{ i18n "pages.index.operationHoursDesc" }}
|
{{ i18n "pages.index.operationHoursDesc" }}
|
||||||
|
@ -176,7 +172,6 @@
|
||||||
</a-layout>
|
</a-layout>
|
||||||
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
|
||||||
:closable="true" @ok="() => versionModal.visible = false"
|
:closable="true" @ok="() => versionModal.visible = false"
|
||||||
:class="siderDrawer.isDarkTheme ? darkClass : ''"
|
|
||||||
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
|
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'>
|
||||||
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
|
||||||
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
|
||||||
|
@ -318,7 +313,6 @@
|
||||||
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
|
||||||
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
|
||||||
okText: '{{ i18n "confirm"}}',
|
okText: '{{ i18n "confirm"}}',
|
||||||
class: siderDrawer.isDarkTheme ? darkClass : '',
|
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
versionModal.hide();
|
versionModal.hide();
|
||||||
|
|
|
@ -40,7 +40,7 @@
|
||||||
|
|
||||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
|
||||||
|
@ -56,7 +56,6 @@
|
||||||
ref="selectLang"
|
ref="selectLang"
|
||||||
v-model="lang"
|
v-model="lang"
|
||||||
@change="setLang(lang)"
|
@change="setLang(lang)"
|
||||||
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
|
|
||||||
style="width: 100%"
|
style="width: 100%"
|
||||||
>
|
>
|
||||||
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
|
||||||
|
@ -88,28 +87,13 @@
|
||||||
style="max-width: 300px"></a-input>
|
style="max-width: 300px"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
|
<!-- <a-button type="primary" @click="updateUser">update</a-button>-->
|
||||||
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
|
||||||
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
|
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
|
|
||||||
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
|
|
||||||
<a-collapse>
|
|
||||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigInbounds"}}">
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model ="inboundSettings"></setting-list-item>
|
|
||||||
</a-collapse-panel>
|
|
||||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigOutbounds"}}">
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model ="outboundSettings"></setting-list-item>
|
|
||||||
</a-collapse-panel>
|
|
||||||
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigRoutings"}}">
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model ="routingRuleSettings"></setting-list-item>
|
|
||||||
</a-collapse-panel>
|
|
||||||
</a-collapse>
|
|
||||||
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
|
|
||||||
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
|
@ -119,10 +103,6 @@
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
||||||
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
|
||||||
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
|
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}' v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
|
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyTrafficDiff" }}' desc='{{ i18n "pages.setting.tgNotifyTrafficDiffDesc" }}' v-model="allSetting.tgTrafficDiff" :min="0"></setting-list-item>
|
|
||||||
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
|
|
||||||
</a-list>
|
</a-list>
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
|
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
|
||||||
|
@ -209,102 +189,6 @@
|
||||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
templateSettings: {
|
|
||||||
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null ; },
|
|
||||||
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
|
|
||||||
},
|
|
||||||
inboundSettings: {
|
|
||||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = this.templateSettings;
|
|
||||||
newTemplateSettings.inbounds = JSON.parse(newValue)
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
outboundSettings: {
|
|
||||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = this.templateSettings;
|
|
||||||
newTemplateSettings.outbounds = JSON.parse(newValue)
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
routingRuleSettings: {
|
|
||||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = this.templateSettings;
|
|
||||||
newTemplateSettings.routing.rules = JSON.parse(newValue)
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
torrentSettings: {
|
|
||||||
get: function () {
|
|
||||||
torrentFilter = false
|
|
||||||
if(this.templateSettings != null){
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if(routingRule.hasOwnProperty("protocol")){
|
|
||||||
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
|
||||||
torrentFilter = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return torrentFilter
|
|
||||||
},
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
|
||||||
if (newValue){
|
|
||||||
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newTemplateSettings.routing.rules = [];
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if (routingRule.hasOwnProperty('protocol')){
|
|
||||||
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newTemplateSettings.routing.rules.push(routingRule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
privateIpSettings: {
|
|
||||||
get: function () {
|
|
||||||
localIpFilter = false
|
|
||||||
if(this.templateSettings != null){
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if(routingRule.hasOwnProperty("ip")){
|
|
||||||
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
|
||||||
localIpFilter = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return localIpFilter
|
|
||||||
},
|
|
||||||
set: function (newValue) {
|
|
||||||
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
|
|
||||||
if (newValue){
|
|
||||||
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
newTemplateSettings.routing.rules = [];
|
|
||||||
this.templateSettings.routing.rules.forEach(routingRule => {
|
|
||||||
if (routingRule.hasOwnProperty('ip')){
|
|
||||||
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
newTemplateSettings.routing.rules.push(routingRule);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.templateSettings = newTemplateSettings
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,30 +0,0 @@
|
||||||
package job
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
"x-ui/web/service"
|
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v3/cpu"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CheckCpuJob struct {
|
|
||||||
tgbotService service.Tgbot
|
|
||||||
settingService service.SettingService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCheckCpuJob() *CheckCpuJob {
|
|
||||||
return new(CheckCpuJob)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
|
||||||
func (j *CheckCpuJob) Run() {
|
|
||||||
threshold, _ := j.settingService.GetTgCpu()
|
|
||||||
|
|
||||||
// get latest status of server
|
|
||||||
percent, err := cpu.Percent(1*time.Second, false)
|
|
||||||
if err == nil && percent[0] > float64(threshold) {
|
|
||||||
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
|
|
||||||
j.tgbotService.SendMsgToTgbotAdmins(msg)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,15 @@
|
||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
"x-ui/logger"
|
||||||
|
"x-ui/util/common"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
|
@ -12,18 +20,229 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatsNotifyJob struct {
|
type StatsNotifyJob struct {
|
||||||
|
enable bool
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
tgbotService service.Tgbot
|
inboundService service.InboundService
|
||||||
|
settingService service.SettingService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatsNotifyJob() *StatsNotifyJob {
|
func NewStatsNotifyJob() *StatsNotifyJob {
|
||||||
return new(StatsNotifyJob)
|
return new(StatsNotifyJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
|
||||||
|
//Telegram bot basic info
|
||||||
|
tgBottoken, err := j.settingService.GetTgBotToken()
|
||||||
|
if err != nil || tgBottoken == "" {
|
||||||
|
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tgBotid, err := j.settingService.GetTgBotChatId()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
bot, err := tgbotapi.NewBotAPI(tgBottoken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("get tgbot error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
bot.Debug = true
|
||||||
|
fmt.Printf("Authorized on account %s", bot.Self.UserName)
|
||||||
|
info := tgbotapi.NewMessage(int64(tgBotid), msg)
|
||||||
|
//msg.ReplyToMessageID = int(tgBotid)
|
||||||
|
bot.Send(info)
|
||||||
|
}
|
||||||
|
|
||||||
// Here run is a interface method of Job interface
|
// Here run is a interface method of Job interface
|
||||||
func (j *StatsNotifyJob) Run() {
|
func (j *StatsNotifyJob) Run() {
|
||||||
if !j.xrayService.IsXrayRunning() {
|
if !j.xrayService.IsXrayRunning() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
j.tgbotService.SendReport()
|
var info string
|
||||||
|
//get hostname
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("get hostname error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info = fmt.Sprintf("Hostname:%s\r\n", name)
|
||||||
|
//get ip address
|
||||||
|
var ip string
|
||||||
|
netInterfaces, err := net.Interfaces()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("net.Interfaces failed, err:", err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(netInterfaces); i++ {
|
||||||
|
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
||||||
|
addrs, _ := netInterfaces[i].Addrs()
|
||||||
|
|
||||||
|
for _, address := range addrs {
|
||||||
|
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||||
|
if ipnet.IP.To4() != nil {
|
||||||
|
ip = ipnet.IP.String()
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
ip = ipnet.IP.String()
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info += fmt.Sprintf("IP:%s\r\n \r\n", ip)
|
||||||
|
|
||||||
|
// get traffic
|
||||||
|
inbouds, err := j.inboundService.GetAllInbounds()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("StatsNotifyJob run failed:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// NOTE:If there no any sessions here,need to notify here
|
||||||
|
// TODO:Sub-node push, automatic conversion format
|
||||||
|
for _, inbound := range inbouds {
|
||||||
|
info += fmt.Sprintf("Node name:%s\r\nPort:%d\r\nUpload↑:%s\r\nDownload↓:%s\r\nTotal:%s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
|
||||||
|
if inbound.ExpiryTime == 0 {
|
||||||
|
info += fmt.Sprintf("Expire date:unlimited\r\n \r\n")
|
||||||
|
} else {
|
||||||
|
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
j.SendMsgToTgbot(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
||||||
|
if username == "" || ip == "" || time == "" {
|
||||||
|
logger.Warning("UserLoginNotify failed,invalid info")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var msg string
|
||||||
|
// Get hostname
|
||||||
|
name, err := os.Hostname()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("get hostname error:", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if status == LoginSuccess {
|
||||||
|
msg = fmt.Sprintf("Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
|
||||||
|
} else if status == LoginFail {
|
||||||
|
msg = fmt.Sprintf("Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
|
||||||
|
}
|
||||||
|
msg += fmt.Sprintf("Time:%s\r\n", time)
|
||||||
|
msg += fmt.Sprintf("Username:%s\r\n", username)
|
||||||
|
msg += fmt.Sprintf("IP:%s\r\n", ip)
|
||||||
|
j.SendMsgToTgbot(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
||||||
|
tgbotapi.NewInlineKeyboardRow(
|
||||||
|
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "get_usage"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
func (j *StatsNotifyJob) OnReceive() *StatsNotifyJob {
|
||||||
|
tgBottoken, err := j.settingService.GetTgBotToken()
|
||||||
|
if err != nil || tgBottoken == "" {
|
||||||
|
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
bot, err := tgbotapi.NewBotAPI(tgBottoken)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("get tgbot error:", err)
|
||||||
|
return j
|
||||||
|
}
|
||||||
|
bot.Debug = false
|
||||||
|
u := tgbotapi.NewUpdate(0)
|
||||||
|
u.Timeout = 10
|
||||||
|
|
||||||
|
updates := bot.GetUpdatesChan(u)
|
||||||
|
|
||||||
|
for update := range updates {
|
||||||
|
if update.Message == nil {
|
||||||
|
|
||||||
|
if update.CallbackQuery != nil {
|
||||||
|
// Respond to the callback query, telling Telegram to show the user
|
||||||
|
// a message with the data received.
|
||||||
|
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
|
||||||
|
if _, err := bot.Request(callback); err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And finally, send a message containing the data received.
|
||||||
|
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, "")
|
||||||
|
|
||||||
|
switch update.CallbackQuery.Data {
|
||||||
|
case "get_usage":
|
||||||
|
msg.Text = "for get your usage send command like this : \n <code>/usage uuid | id</code> \n example : <code>/usage fc3239ed-8f3b-4151-ff51-b183d5182142</code>"
|
||||||
|
msg.ParseMode = "HTML"
|
||||||
|
}
|
||||||
|
if _, err := bot.Send(msg); err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if !update.Message.IsCommand() { // ignore any non-command Messages
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new MessageConfig. We don't have text yet,
|
||||||
|
// so we leave it empty.
|
||||||
|
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
|
||||||
|
|
||||||
|
// Extract the command from the Message.
|
||||||
|
switch update.Message.Command() {
|
||||||
|
case "help":
|
||||||
|
msg.Text = "What you need?"
|
||||||
|
msg.ReplyMarkup = numericKeyboard
|
||||||
|
case "start":
|
||||||
|
msg.Text = "Hi :) \n What you need?"
|
||||||
|
msg.ReplyMarkup = numericKeyboard
|
||||||
|
|
||||||
|
case "status":
|
||||||
|
msg.Text = "bot is ok."
|
||||||
|
|
||||||
|
case "usage":
|
||||||
|
msg.Text = j.getClientUsage(update.Message.CommandArguments())
|
||||||
|
default:
|
||||||
|
msg.Text = "I don't know that command, /help"
|
||||||
|
msg.ReplyMarkup = numericKeyboard
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := bot.Send(msg); err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return j
|
||||||
|
|
||||||
|
}
|
||||||
|
func (j *StatsNotifyJob) getClientUsage(id string) string {
|
||||||
|
traffic, err := j.inboundService.GetClientTrafficById(id)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
return "something wrong!"
|
||||||
|
}
|
||||||
|
expiryTime := ""
|
||||||
|
if traffic.ExpiryTime == 0 {
|
||||||
|
expiryTime = fmt.Sprintf("unlimited")
|
||||||
|
} else {
|
||||||
|
expiryTime = fmt.Sprintf("%s", time.Unix((traffic.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||||
|
}
|
||||||
|
total := ""
|
||||||
|
if traffic.Total == 0 {
|
||||||
|
total = fmt.Sprintf("unlimited")
|
||||||
|
} else {
|
||||||
|
total = fmt.Sprintf("%s", common.FormatTraffic((traffic.Total)))
|
||||||
|
}
|
||||||
|
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Download↑: %s\r\n🔽 Upload↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
||||||
|
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
||||||
|
total, expiryTime)
|
||||||
|
|
||||||
|
return output
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,4 +34,5 @@ func (j *XrayTrafficJob) Run() {
|
||||||
logger.Warning("add client traffic failed:", err)
|
logger.Warning("add client traffic failed:", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -54,7 +54,7 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
|
||||||
settings := map[string][]model.Client{}
|
settings := map[string][]model.Client{}
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
if settings == nil {
|
if settings == nil {
|
||||||
return nil, fmt.Errorf("setting is null")
|
return nil, fmt.Errorf("Setting is null")
|
||||||
}
|
}
|
||||||
|
|
||||||
clients := settings["clients"]
|
clients := settings["clients"]
|
||||||
|
@ -125,18 +125,11 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
|
||||||
return inbound, common.NewError("Duplicate email:", existEmail)
|
return inbound, common.NewError("Duplicate email:", existEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
clients, err := s.getClients(inbound)
|
|
||||||
if err != nil {
|
|
||||||
return inbound, err
|
|
||||||
}
|
|
||||||
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
err = db.Save(inbound).Error
|
err = db.Save(inbound).Error
|
||||||
if err == nil {
|
if err == nil {
|
||||||
for _, client := range clients {
|
s.UpdateClientStat(inbound.Id, inbound.Settings)
|
||||||
s.AddClientStat(inbound.Id, &client)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return inbound, err
|
return inbound, err
|
||||||
}
|
}
|
||||||
|
@ -175,24 +168,6 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
|
||||||
|
|
||||||
func (s *InboundService) DelInbound(id int) error {
|
func (s *InboundService) DelInbound(id int) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
inbound, err := s.GetInbound(id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
clients, err := s.getClients(inbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, client := range clients {
|
|
||||||
err := s.DelClientIPs(db, client.Email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return db.Delete(model.Inbound{}, id).Error
|
return db.Delete(model.Inbound{}, id).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,128 +216,11 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||||
oldInbound.Sniffing = inbound.Sniffing
|
oldInbound.Sniffing = inbound.Sniffing
|
||||||
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||||
|
|
||||||
|
s.UpdateClientStat(inbound.Id, inbound.Settings)
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
return inbound, db.Save(oldInbound).Error
|
return inbound, db.Save(oldInbound).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
|
|
||||||
existEmail, err := s.checkEmailExistForInbound(inbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if existEmail != "" {
|
|
||||||
return common.NewError("Duplicate email:", existEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
clients, err := s.getClients(inbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound, err := s.GetInbound(inbound.Id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldClients, err := s.getClients(oldInbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound.Settings = inbound.Settings
|
|
||||||
|
|
||||||
if len(clients[len(clients)-1].Email) > 0 {
|
|
||||||
s.AddClientStat(inbound.Id, &clients[len(clients)-1])
|
|
||||||
}
|
|
||||||
for i := len(oldClients); i < len(clients); i++ {
|
|
||||||
if len(clients[i].Email) > 0 {
|
|
||||||
s.AddClientStat(inbound.Id, &clients[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
db := database.GetDB()
|
|
||||||
return db.Save(oldInbound).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string) error {
|
|
||||||
db := database.GetDB()
|
|
||||||
err := s.DelClientStat(db, email)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Delete stats Data Error")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound, err := s.GetInbound(inbound.Id)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Load Old Data Error")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound.Settings = inbound.Settings
|
|
||||||
|
|
||||||
err = s.DelClientIPs(db, email)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Error in delete client IPs")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.Save(oldInbound).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int) error {
|
|
||||||
existEmail, err := s.checkEmailExistForInbound(inbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if existEmail != "" {
|
|
||||||
return common.NewError("Duplicate email:", existEmail)
|
|
||||||
}
|
|
||||||
|
|
||||||
clients, err := s.getClients(inbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound, err := s.GetInbound(inbound.Id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldClients, err := s.getClients(oldInbound)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
oldInbound.Settings = inbound.Settings
|
|
||||||
|
|
||||||
db := database.GetDB()
|
|
||||||
|
|
||||||
if len(clients[index].Email) > 0 {
|
|
||||||
if len(oldClients[index].Email) > 0 {
|
|
||||||
err = s.UpdateClientStat(oldClients[index].Email, &clients[index])
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = s.UpdateClientIPs(db, oldClients[index].Email, clients[index].Email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
s.AddClientStat(inbound.Id, &clients[index])
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
err = s.DelClientStat(db, oldClients[index].Email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = s.DelClientIPs(db, oldClients[index].Email)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return db.Save(oldInbound).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
||||||
if len(traffics) == 0 {
|
if len(traffics) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
@ -425,12 +283,10 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
|
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
logger.Warning(err, traffic.Email)
|
logger.Warning(err, traffic.Email)
|
||||||
|
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
@ -444,7 +300,7 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
|
||||||
traffic.Total = client.TotalGB
|
traffic.Total = client.TotalGB
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if tx.Where("inbound_id = ? and email = ?", inbound.Id, traffic.Email).
|
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email).
|
||||||
UpdateColumns(map[string]interface{}{
|
UpdateColumns(map[string]interface{}{
|
||||||
"enable": true,
|
"enable": true,
|
||||||
"expiry_time": traffic.ExpiryTime,
|
"expiry_time": traffic.ExpiryTime,
|
||||||
|
@ -483,9 +339,18 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
|
||||||
count := result.RowsAffected
|
count := result.RowsAffected
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
|
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
|
// get settings clients
|
||||||
|
settings := map[string][]model.Client{}
|
||||||
|
json.Unmarshal([]byte(inboundSettings), &settings)
|
||||||
|
clients := settings["clients"]
|
||||||
|
for _, client := range clients {
|
||||||
|
result := db.Model(xray.ClientTraffic{}).
|
||||||
|
Where("inbound_id = ? and email = ?", inboundId, client.Email).
|
||||||
|
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime})
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
clientTraffic := xray.ClientTraffic{}
|
clientTraffic := xray.ClientTraffic{}
|
||||||
clientTraffic.InboundId = inboundId
|
clientTraffic.InboundId = inboundId
|
||||||
clientTraffic.Email = client.Email
|
clientTraffic.Email = client.Email
|
||||||
|
@ -494,49 +359,48 @@ func (s *InboundService) AddClientStat(inboundId int, client *model.Client) erro
|
||||||
clientTraffic.Enable = true
|
clientTraffic.Enable = true
|
||||||
clientTraffic.Up = 0
|
clientTraffic.Up = 0
|
||||||
clientTraffic.Down = 0
|
clientTraffic.Down = 0
|
||||||
result := db.Create(&clientTraffic)
|
db.Create(&clientTraffic)
|
||||||
|
}
|
||||||
err := result.Error
|
err := result.Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}
|
|
||||||
func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
|
|
||||||
db := database.GetDB()
|
|
||||||
|
|
||||||
result := db.Model(xray.ClientTraffic{}).
|
|
||||||
Where("email = ?", email).
|
|
||||||
Updates(map[string]interface{}{
|
|
||||||
"enable": true,
|
|
||||||
"email": client.Email,
|
|
||||||
"total": client.TotalGB,
|
|
||||||
"expiry_time": client.ExpiryTime})
|
|
||||||
err := result.Error
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
|
|
||||||
return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
|
func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
|
||||||
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
|
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
|
||||||
}
|
}
|
||||||
|
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
|
||||||
func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
|
db := database.GetDB()
|
||||||
logger.Warning(email)
|
InboundClientIps := &model.InboundClientIps{}
|
||||||
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
|
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
}
|
}
|
||||||
|
return InboundClientIps.Ips, nil
|
||||||
|
}
|
||||||
|
func (s *InboundService) ClearClientIps(clientEmail string) (error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
|
result := db.Model(model.InboundClientIps{}).
|
||||||
|
Where("client_email = ?", clientEmail).
|
||||||
|
Update("ips", "")
|
||||||
|
err := result.Error
|
||||||
|
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (s *InboundService) ResetClientTraffic(clientEmail string) error {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
result := db.Model(xray.ClientTraffic{}).
|
result := db.Model(xray.ClientTraffic{}).
|
||||||
Where("inbound_id = ? and email = ?", id, clientEmail).
|
Where("email = ?", clientEmail).
|
||||||
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
|
Updates(map[string]interface{}{"up": 0, "down": 0})
|
||||||
|
|
||||||
err := result.Error
|
err := result.Error
|
||||||
|
|
||||||
|
@ -545,40 +409,12 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
func (s *InboundService) GetClientTrafficTgBot(tguname string) (traffic []*xray.ClientTraffic, err error) {
|
func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.ClientTraffic, err error) {
|
||||||
db := database.GetDB()
|
|
||||||
var traffics []*xray.ClientTraffic
|
|
||||||
|
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%@"+tguname).Find(&traffics).Error
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
logger.Warning(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return traffics, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) {
|
|
||||||
db := database.GetDB()
|
|
||||||
var traffics []*xray.ClientTraffic
|
|
||||||
|
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%"+email+"%").Find(&traffics).Error
|
|
||||||
if err != nil {
|
|
||||||
if err == gorm.ErrRecordNotFound {
|
|
||||||
logger.Warning(err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return traffics, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
inbound := &model.Inbound{}
|
inbound := &model.Inbound{}
|
||||||
traffic = &xray.ClientTraffic{}
|
traffic = &xray.ClientTraffic{}
|
||||||
|
|
||||||
err = db.Model(model.Inbound{}).Where("settings like ?", "%\""+query+"\"%").First(inbound).Error
|
err = db.Model(model.Inbound{}).Where("settings like ?", "%"+uuid+"%").First(inbound).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == gorm.ErrRecordNotFound {
|
if err == gorm.ErrRecordNotFound {
|
||||||
logger.Warning(err)
|
logger.Warning(err)
|
||||||
|
@ -592,17 +428,9 @@ func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.Client
|
||||||
json.Unmarshal([]byte(inbound.Settings), &settings)
|
json.Unmarshal([]byte(inbound.Settings), &settings)
|
||||||
clients := settings["clients"]
|
clients := settings["clients"]
|
||||||
for _, client := range clients {
|
for _, client := range clients {
|
||||||
if client.ID == query && client.Email != "" {
|
if uuid == client.ID {
|
||||||
traffic.Email = client.Email
|
traffic.Email = client.Email
|
||||||
break
|
|
||||||
}
|
}
|
||||||
if client.Password == query && client.Email != "" {
|
|
||||||
traffic.Email = client.Email
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if traffic.Email == "" {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
|
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -611,26 +439,3 @@ func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.Client
|
||||||
}
|
}
|
||||||
return traffic, err
|
return traffic, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
|
|
||||||
db := database.GetDB()
|
|
||||||
InboundClientIps := &model.InboundClientIps{}
|
|
||||||
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return InboundClientIps.Ips, nil
|
|
||||||
}
|
|
||||||
func (s *InboundService) ClearClientIps(clientEmail string) error {
|
|
||||||
db := database.GetDB()
|
|
||||||
|
|
||||||
result := db.Model(model.InboundClientIps{}).
|
|
||||||
Where("client_email = ?", clientEmail).
|
|
||||||
Update("ips", "")
|
|
||||||
err := result.Error
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
|
@ -31,12 +31,8 @@ var defaultValueMap = map[string]string{
|
||||||
"timeLocation": "Asia/Tehran",
|
"timeLocation": "Asia/Tehran",
|
||||||
"tgBotEnable": "false",
|
"tgBotEnable": "false",
|
||||||
"tgBotToken": "",
|
"tgBotToken": "",
|
||||||
"tgBotChatId": "",
|
"tgBotChatId": "0",
|
||||||
"tgRunTime": "@daily",
|
"tgRunTime": "",
|
||||||
"tgBotBackup": "false",
|
|
||||||
"tgExpireDiff": "0",
|
|
||||||
"tgTrafficDiff": "0",
|
|
||||||
"tgCpu": "0",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingService struct {
|
type SettingService struct {
|
||||||
|
@ -206,60 +202,28 @@ func (s *SettingService) SetTgBotToken(token string) error {
|
||||||
return s.setString("tgBotToken", token)
|
return s.setString("tgBotToken", token)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgBotChatId() (string, error) {
|
func (s *SettingService) GetTgBotChatId() (int, error) {
|
||||||
return s.getString("tgBotChatId")
|
return s.getInt("tgBotChatId")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) SetTgBotChatId(chatIds string) error {
|
func (s *SettingService) SetTgBotChatId(chatId int) error {
|
||||||
return s.setString("tgBotChatId", chatIds)
|
return s.setInt("tgBotChatId", chatId)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) GetTgbotenabled() (bool, error) {
|
|
||||||
return s.getBool("tgBotEnable")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) SetTgbotenabled(value bool) error {
|
func (s *SettingService) SetTgbotenabled(value bool) error {
|
||||||
return s.setBool("tgBotEnable", value)
|
return s.setBool("tgBotEnable", value)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
func (s *SettingService) GetTgbotenabled() (bool, error) {
|
||||||
return s.getString("tgRunTime")
|
return s.getBool("tgBotEnable")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) SetTgbotRuntime(time string) error {
|
func (s *SettingService) SetTgbotRuntime(time string) error {
|
||||||
return s.setString("tgRunTime", time)
|
return s.setString("tgRunTime", time)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetTgBotBackup() (bool, error) {
|
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
||||||
return s.getBool("tgBotBackup")
|
return s.getString("tgRunTime")
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) SetTgBotBackup(value bool) error {
|
|
||||||
return s.setBool("tgBotBackup", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) GetTgExpireDiff() (int, error) {
|
|
||||||
return s.getInt("tgExpireDiff")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) SetTgExpireDiff(value int) error {
|
|
||||||
return s.setInt("tgExpireDiff", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) GetTgTrafficDiff() (int, error) {
|
|
||||||
return s.getInt("tgTrafficDiff")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) SetTgTrafficDiff(value int) error {
|
|
||||||
return s.setInt("tgTrafficDiff", value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) GetTgCpu() (int, error) {
|
|
||||||
return s.getInt("tgCpu")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SettingService) SetTgCpu(value int) error {
|
|
||||||
return s.setInt("tgCpu", value)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetPort() (int, error) {
|
func (s *SettingService) GetPort() (int, error) {
|
||||||
|
|
|
@ -1,546 +0,0 @@
|
||||||
package service
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
"x-ui/config"
|
|
||||||
"x-ui/database/model"
|
|
||||||
"x-ui/logger"
|
|
||||||
"x-ui/util/common"
|
|
||||||
"x-ui/xray"
|
|
||||||
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
|
||||||
)
|
|
||||||
|
|
||||||
var bot *tgbotapi.BotAPI
|
|
||||||
var adminIds []int64
|
|
||||||
var isRunning bool
|
|
||||||
|
|
||||||
type LoginStatus byte
|
|
||||||
|
|
||||||
const (
|
|
||||||
LoginSuccess LoginStatus = 1
|
|
||||||
LoginFail LoginStatus = 0
|
|
||||||
)
|
|
||||||
|
|
||||||
type Tgbot struct {
|
|
||||||
inboundService InboundService
|
|
||||||
settingService SettingService
|
|
||||||
serverService ServerService
|
|
||||||
lastStatus *Status
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) NewTgbot() *Tgbot {
|
|
||||||
return new(Tgbot)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) Start() error {
|
|
||||||
tgBottoken, err := t.settingService.GetTgBotToken()
|
|
||||||
if err != nil || tgBottoken == "" {
|
|
||||||
logger.Warning("Get TgBotToken failed:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
tgBotid, err := t.settingService.GetTgBotChatId()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Get GetTgBotChatId failed:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, adminId := range strings.Split(tgBotid, ",") {
|
|
||||||
id, err := strconv.Atoi(adminId)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
adminIds = append(adminIds, int64(id))
|
|
||||||
}
|
|
||||||
|
|
||||||
bot, err = tgbotapi.NewBotAPI(tgBottoken)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println("Get tgbot's api error:", err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
bot.Debug = false
|
|
||||||
|
|
||||||
// listen for TG bot income messages
|
|
||||||
if !isRunning {
|
|
||||||
logger.Info("Starting Telegram receiver ...")
|
|
||||||
go t.OnReceive()
|
|
||||||
isRunning = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) IsRunnging() bool {
|
|
||||||
return isRunning
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) Stop() {
|
|
||||||
bot.StopReceivingUpdates()
|
|
||||||
logger.Info("Stop Telegram receiver ...")
|
|
||||||
isRunning = false
|
|
||||||
adminIds = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) OnReceive() {
|
|
||||||
u := tgbotapi.NewUpdate(0)
|
|
||||||
u.Timeout = 10
|
|
||||||
|
|
||||||
updates := bot.GetUpdatesChan(u)
|
|
||||||
|
|
||||||
for update := range updates {
|
|
||||||
tgId := update.FromChat().ID
|
|
||||||
chatId := update.FromChat().ChatConfig().ChatID
|
|
||||||
isAdmin := checkAdmin(tgId)
|
|
||||||
if update.Message == nil {
|
|
||||||
if update.CallbackQuery != nil {
|
|
||||||
t.asnwerCallback(update.CallbackQuery, isAdmin)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if update.Message.IsCommand() {
|
|
||||||
t.answerCommand(update.Message, chatId, isAdmin)
|
|
||||||
} else {
|
|
||||||
t.aswerChat(update.Message.Text, chatId, isAdmin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
|
|
||||||
msg := ""
|
|
||||||
// Extract the command from the Message.
|
|
||||||
switch message.Command() {
|
|
||||||
case "help":
|
|
||||||
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
|
|
||||||
case "start":
|
|
||||||
msg = "Hello <i>" + message.From.FirstName + "</i> 👋"
|
|
||||||
if isAdmin {
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
|
|
||||||
}
|
|
||||||
msg += "\n\nI can do some magics for you, please choose:"
|
|
||||||
case "status":
|
|
||||||
msg = "bot is ok ✅"
|
|
||||||
case "usage":
|
|
||||||
if isAdmin {
|
|
||||||
t.searchClient(chatId, message.CommandArguments())
|
|
||||||
} else {
|
|
||||||
t.searchForClient(chatId, message.CommandArguments())
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
msg = "❗ Unknown command"
|
|
||||||
}
|
|
||||||
t.SendAnswer(chatId, msg, isAdmin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) aswerChat(message string, chatId int64, isAdmin bool) {
|
|
||||||
t.SendAnswer(chatId, "❗ Unknown message", isAdmin)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
|
|
||||||
// Respond to the callback query, telling Telegram to show the user
|
|
||||||
// a message with the data received.
|
|
||||||
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
|
|
||||||
if _, err := bot.Request(callback); err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch callbackQuery.Data {
|
|
||||||
case "get_usage":
|
|
||||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
|
|
||||||
case "inbounds":
|
|
||||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
|
|
||||||
case "exhausted_soon":
|
|
||||||
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
|
|
||||||
case "get_backup":
|
|
||||||
t.sendBackup(callbackQuery.From.ID)
|
|
||||||
case "client_traffic":
|
|
||||||
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
|
|
||||||
case "client_commands":
|
|
||||||
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess and vless and Password for Trojan.")
|
|
||||||
case "commands":
|
|
||||||
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for a client email, just use folowing command:\r\n \r\n<code>/usage email</code>")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func checkAdmin(tgId int64) bool {
|
|
||||||
for _, adminId := range adminIds {
|
|
||||||
if adminId == tgId {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
|
||||||
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
|
|
||||||
tgbotapi.NewInlineKeyboardRow(
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"),
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"),
|
|
||||||
),
|
|
||||||
tgbotapi.NewInlineKeyboardRow(
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Exhausted soon", "exhausted_soon"),
|
|
||||||
),
|
|
||||||
tgbotapi.NewInlineKeyboardRow(
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
|
|
||||||
tgbotapi.NewInlineKeyboardRow(
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
|
|
||||||
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
msgConfig := tgbotapi.NewMessage(chatId, msg)
|
|
||||||
msgConfig.ParseMode = "HTML"
|
|
||||||
if isAdmin {
|
|
||||||
msgConfig.ReplyMarkup = numericKeyboard
|
|
||||||
} else {
|
|
||||||
msgConfig.ReplyMarkup = numericKeyboardClient
|
|
||||||
}
|
|
||||||
_, err := bot.Send(msgConfig)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Error sending telegram message :", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
|
|
||||||
var allMessages []string
|
|
||||||
limit := 2000
|
|
||||||
// paging message if it is big
|
|
||||||
if len(msg) > limit {
|
|
||||||
messages := strings.Split(msg, "\r\n \r\n")
|
|
||||||
lastIndex := -1
|
|
||||||
for _, message := range messages {
|
|
||||||
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
|
|
||||||
allMessages = append(allMessages, message)
|
|
||||||
lastIndex++
|
|
||||||
} else {
|
|
||||||
allMessages[lastIndex] += "\r\n \r\n" + message
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
allMessages = append(allMessages, msg)
|
|
||||||
}
|
|
||||||
for _, message := range allMessages {
|
|
||||||
info := tgbotapi.NewMessage(tgid, message)
|
|
||||||
info.ParseMode = "HTML"
|
|
||||||
_, err := bot.Send(info)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Error sending telegram message :", err)
|
|
||||||
}
|
|
||||||
time.Sleep(500 * time.Millisecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
|
|
||||||
for _, adminId := range adminIds {
|
|
||||||
t.SendMsgToTgbot(adminId, msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) SendReport() {
|
|
||||||
runTime, err := t.settingService.GetTgbotRuntime()
|
|
||||||
if err == nil && len(runTime) > 0 {
|
|
||||||
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
info := t.getServerUsage()
|
|
||||||
t.SendMsgToTgbotAdmins(info)
|
|
||||||
exhausted := t.getExhausted()
|
|
||||||
t.SendMsgToTgbotAdmins(exhausted)
|
|
||||||
backupEnable, err := t.settingService.GetTgBotBackup()
|
|
||||||
if err == nil && backupEnable {
|
|
||||||
for _, adminId := range adminIds {
|
|
||||||
t.sendBackup(int64(adminId))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) getServerUsage() string {
|
|
||||||
var info string
|
|
||||||
//get hostname
|
|
||||||
name, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("get hostname error:", err)
|
|
||||||
name = ""
|
|
||||||
}
|
|
||||||
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
|
|
||||||
//get ip address
|
|
||||||
var ip string
|
|
||||||
var ipv6 string
|
|
||||||
netInterfaces, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("net.Interfaces failed, err:", err.Error())
|
|
||||||
info += "🌐 IP: Unknown\r\n \r\n"
|
|
||||||
} else {
|
|
||||||
for i := 0; i < len(netInterfaces); i++ {
|
|
||||||
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
|
||||||
addrs, _ := netInterfaces[i].Addrs()
|
|
||||||
|
|
||||||
for _, address := range addrs {
|
|
||||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
|
||||||
if ipnet.IP.To4() != nil {
|
|
||||||
ip += ipnet.IP.String() + " "
|
|
||||||
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
|
|
||||||
ipv6 += ipnet.IP.String() + " "
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
|
|
||||||
}
|
|
||||||
|
|
||||||
// get latest status of server
|
|
||||||
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
|
|
||||||
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400))
|
|
||||||
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2])
|
|
||||||
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
|
|
||||||
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount)
|
|
||||||
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount)
|
|
||||||
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
|
|
||||||
info += fmt.Sprintf("ℹXray status: %s", t.lastStatus.Xray.State)
|
|
||||||
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
|
||||||
if username == "" || ip == "" || time == "" {
|
|
||||||
logger.Warning("UserLoginNotify failed,invalid info")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var msg string
|
|
||||||
// Get hostname
|
|
||||||
name, err := os.Hostname()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("get hostname error:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if status == LoginSuccess {
|
|
||||||
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
|
|
||||||
} else if status == LoginFail {
|
|
||||||
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
|
|
||||||
}
|
|
||||||
msg += fmt.Sprintf("⏰ Time:%s\r\n", time)
|
|
||||||
msg += fmt.Sprintf("🆔 Username:%s\r\n", username)
|
|
||||||
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip)
|
|
||||||
t.SendMsgToTgbotAdmins(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) getInboundUsages() string {
|
|
||||||
info := ""
|
|
||||||
// get traffic
|
|
||||||
inbouds, err := t.inboundService.GetAllInbounds()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("GetAllInbounds run failed:", err)
|
|
||||||
info += "❌ Failed to get inbounds"
|
|
||||||
} else {
|
|
||||||
// NOTE:If there no any sessions here,need to notify here
|
|
||||||
// TODO:Sub-node push, automatic conversion format
|
|
||||||
for _, inbound := range inbouds {
|
|
||||||
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
|
|
||||||
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
|
||||||
if inbound.ExpiryTime == 0 {
|
|
||||||
info += "Expire date: ♾ Unlimited\r\n \r\n"
|
|
||||||
} else {
|
|
||||||
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return info
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
|
|
||||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
msg := "❌ Something went wrong!"
|
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(traffics) == 0 {
|
|
||||||
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
|
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
|
||||||
}
|
|
||||||
for _, traffic := range traffics {
|
|
||||||
expiryTime := ""
|
|
||||||
if traffic.ExpiryTime == 0 {
|
|
||||||
expiryTime = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
total := ""
|
|
||||||
if traffic.Total == 0 {
|
|
||||||
total = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
total = common.FormatTraffic((traffic.Total))
|
|
||||||
}
|
|
||||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
|
||||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
|
||||||
total, expiryTime)
|
|
||||||
t.SendMsgToTgbot(chatId, output)
|
|
||||||
}
|
|
||||||
t.SendAnswer(chatId, "Please choose:", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) searchClient(chatId int64, email string) {
|
|
||||||
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
msg := "❌ Something went wrong!"
|
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if len(traffics) == 0 {
|
|
||||||
msg := "No result!"
|
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
for _, traffic := range traffics {
|
|
||||||
expiryTime := ""
|
|
||||||
if traffic.ExpiryTime == 0 {
|
|
||||||
expiryTime = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
total := ""
|
|
||||||
if traffic.Total == 0 {
|
|
||||||
total = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
total = common.FormatTraffic((traffic.Total))
|
|
||||||
}
|
|
||||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
|
||||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
|
||||||
total, expiryTime)
|
|
||||||
t.SendMsgToTgbot(chatId, output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) searchForClient(chatId int64, query string) {
|
|
||||||
traffic, err := t.inboundService.SearchClientTraffic(query)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning(err)
|
|
||||||
msg := "❌ Something went wrong!"
|
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if traffic == nil {
|
|
||||||
msg := "No result!"
|
|
||||||
t.SendMsgToTgbot(chatId, msg)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
expiryTime := ""
|
|
||||||
if traffic.ExpiryTime == 0 {
|
|
||||||
expiryTime = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
total := ""
|
|
||||||
if traffic.Total == 0 {
|
|
||||||
total = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
total = common.FormatTraffic((traffic.Total))
|
|
||||||
}
|
|
||||||
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
|
||||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
|
||||||
total, expiryTime)
|
|
||||||
t.SendMsgToTgbot(chatId, output)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) getExhausted() string {
|
|
||||||
trDiff := int64(0)
|
|
||||||
exDiff := int64(0)
|
|
||||||
now := time.Now().Unix() * 1000
|
|
||||||
var exhaustedInbounds []model.Inbound
|
|
||||||
var exhaustedClients []xray.ClientTraffic
|
|
||||||
var disabledInbounds []model.Inbound
|
|
||||||
var disabledClients []xray.ClientTraffic
|
|
||||||
output := ""
|
|
||||||
TrafficThreshold, err := t.settingService.GetTgTrafficDiff()
|
|
||||||
if err == nil && TrafficThreshold > 0 {
|
|
||||||
trDiff = int64(TrafficThreshold) * 1073741824
|
|
||||||
}
|
|
||||||
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
|
|
||||||
if err == nil && ExpireThreshold > 0 {
|
|
||||||
exDiff = int64(ExpireThreshold) * 84600
|
|
||||||
}
|
|
||||||
inbounds, err := t.inboundService.GetAllInbounds()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Unable to load Inbounds", err)
|
|
||||||
}
|
|
||||||
for _, inbound := range inbounds {
|
|
||||||
if inbound.Enable {
|
|
||||||
if (inbound.ExpiryTime > 0 && (now-inbound.ExpiryTime < exDiff)) ||
|
|
||||||
(inbound.Total > 0 && (inbound.Total-inbound.Up+inbound.Down < trDiff)) {
|
|
||||||
exhaustedInbounds = append(exhaustedInbounds, *inbound)
|
|
||||||
}
|
|
||||||
if len(inbound.ClientStats) > 0 {
|
|
||||||
for _, client := range inbound.ClientStats {
|
|
||||||
if client.Enable {
|
|
||||||
if (client.ExpiryTime > 0 && (now-client.ExpiryTime < exDiff)) ||
|
|
||||||
(client.Total > 0 && (client.Total-client.Up+client.Down < trDiff)) {
|
|
||||||
exhaustedClients = append(exhaustedClients, client)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
disabledClients = append(disabledClients, client)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
disabledInbounds = append(disabledInbounds, *inbound)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
|
|
||||||
if len(disabledInbounds)+len(exhaustedInbounds) > 0 {
|
|
||||||
output += "Exhausted Inbounds:\r\n"
|
|
||||||
for _, inbound := range exhaustedInbounds {
|
|
||||||
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
|
|
||||||
if inbound.ExpiryTime == 0 {
|
|
||||||
output += "Expire date: ♾Unlimited\r\n \r\n"
|
|
||||||
} else {
|
|
||||||
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
|
|
||||||
if len(disabledClients)+len(exhaustedClients) > 0 {
|
|
||||||
output += "Exhausted Clients:\r\n"
|
|
||||||
for _, traffic := range exhaustedClients {
|
|
||||||
expiryTime := ""
|
|
||||||
if traffic.ExpiryTime == 0 {
|
|
||||||
expiryTime = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
|
||||||
}
|
|
||||||
total := ""
|
|
||||||
if traffic.Total == 0 {
|
|
||||||
total = "♾Unlimited"
|
|
||||||
} else {
|
|
||||||
total = common.FormatTraffic((traffic.Total))
|
|
||||||
}
|
|
||||||
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
|
|
||||||
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
|
|
||||||
total, expiryTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return output
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *Tgbot) sendBackup(chatId int64) {
|
|
||||||
sendingTime := time.Now().Format("2006-01-02 15:04:05")
|
|
||||||
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
|
|
||||||
file := tgbotapi.FilePath(config.GetDBPath())
|
|
||||||
msg := tgbotapi.NewDocument(chatId, file)
|
|
||||||
_, err := bot.Send(msg)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Error in uploading backup: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -160,5 +160,5 @@ func (s *XrayService) SetToNeedRestart() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
|
||||||
return isNeedXrayRestart.CompareAndSwap(true, false)
|
return isNeedXrayRestart.CAS(true, false)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
"username" = "Username"
|
"username" = "username"
|
||||||
"password" = "Password"
|
"password" = "password"
|
||||||
"login" = "Login"
|
"login" = "Login"
|
||||||
"confirm" = "Confirm"
|
"confirm" = "Confirm"
|
||||||
"cancel" = "Cancel"
|
"cancel" = "Cancel"
|
||||||
|
@ -10,8 +10,6 @@
|
||||||
"remark" = "Remark"
|
"remark" = "Remark"
|
||||||
"enable" = "Enable"
|
"enable" = "Enable"
|
||||||
"protocol" = "Protocol"
|
"protocol" = "Protocol"
|
||||||
"search" = "Search"
|
|
||||||
|
|
||||||
"loading" = "Loading"
|
"loading" = "Loading"
|
||||||
"second" = "Second"
|
"second" = "Second"
|
||||||
"minute" = "Minute"
|
"minute" = "Minute"
|
||||||
|
@ -22,7 +20,6 @@
|
||||||
"unlimited" = "Unlimited"
|
"unlimited" = "Unlimited"
|
||||||
"none" = "None"
|
"none" = "None"
|
||||||
"qrCode" = "QR Code"
|
"qrCode" = "QR Code"
|
||||||
"info" = "More information"
|
|
||||||
"edit" = "Edit"
|
"edit" = "Edit"
|
||||||
"delete" = "Delete"
|
"delete" = "Delete"
|
||||||
"reset" = "Reset"
|
"reset" = "Reset"
|
||||||
|
@ -35,16 +32,19 @@
|
||||||
"camouflage" = "Camouflage"
|
"camouflage" = "Camouflage"
|
||||||
"enabled" = "Enabled"
|
"enabled" = "Enabled"
|
||||||
"disabled" = "Disabled"
|
"disabled" = "Disabled"
|
||||||
"domainName" = "Domain name"
|
"domainName" = "Domain Name"
|
||||||
"additional" = "Alter"
|
"additional" = "Alter"
|
||||||
"monitor" = "Listen IP"
|
"monitor" = "Listen IP"
|
||||||
"certificate" = "Certificat"
|
"certificate" = "Certificate"
|
||||||
"fail" = "Fail"
|
"fail" = "Fail"
|
||||||
"success" = "Success"
|
"success" = "Success"
|
||||||
"getVersion" = "Get version"
|
"getVersion" = "Get Version"
|
||||||
"install" = "Install"
|
"install" = "Install"
|
||||||
|
"used" = "Used"
|
||||||
"clients" = "Clients"
|
"clients" = "Clients"
|
||||||
|
"search" = "Search"
|
||||||
"usage" = "Usage"
|
"usage" = "Usage"
|
||||||
|
"info" = "Details"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "System Status"
|
"dashboard" = "System Status"
|
||||||
|
@ -61,17 +61,18 @@
|
||||||
"invalidFormData" = "Input Data Format Is Invalid"
|
"invalidFormData" = "Input Data Format Is Invalid"
|
||||||
"emptyUsername" = "Please Enter Username"
|
"emptyUsername" = "Please Enter Username"
|
||||||
"emptyPassword" = "Please Enter Password"
|
"emptyPassword" = "Please Enter Password"
|
||||||
"wrongUsernameOrPassword" = "Invalid username or password"
|
"wrongUsernameOrPassword" = "invalid username or password"
|
||||||
"successLogin" = "Login"
|
"successLogin" = "Login"
|
||||||
|
|
||||||
|
|
||||||
[pages.index]
|
[pages.index]
|
||||||
"title" = "System Status"
|
"title" = "System Status"
|
||||||
"memory" = "Memory"
|
"memory" = "Memory"
|
||||||
"hard" = "Hard Disk"
|
"hard" = "Hard Disk"
|
||||||
"xrayStatus" = "Xray Status"
|
"xrayStatus" = "Xray Status"
|
||||||
"stopXray" = "Stop"
|
|
||||||
"restartXray" = "Restart"
|
|
||||||
"xraySwitch" = "Switch Version"
|
"xraySwitch" = "Switch Version"
|
||||||
|
"restartXray" = "Restart"
|
||||||
|
"stopXray" = "Stop"
|
||||||
"xraySwitchClick" = "Click on the version you want to switch"
|
"xraySwitchClick" = "Click on the version you want to switch"
|
||||||
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
|
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
|
||||||
"operationHours" = "Operation Hours"
|
"operationHours" = "Operation Hours"
|
||||||
|
@ -83,15 +84,16 @@
|
||||||
"downSpeed" = "Total download speed for all network cards"
|
"downSpeed" = "Total download speed for all network cards"
|
||||||
"totalSent" = "Total upload traffic of all network cards since system startup"
|
"totalSent" = "Total upload traffic of all network cards since system startup"
|
||||||
"totalReceive" = "Total download traffic of all network cards since system startup"
|
"totalReceive" = "Total download traffic of all network cards since system startup"
|
||||||
"xraySwitchVersionDialog" = "Switch xray version"
|
"xraySwitchVersionDialog" = "switch xray version"
|
||||||
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
|
"xraySwitchVersionDialogDesc" = "whether to switch the xray version to"
|
||||||
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
|
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
|
"export" = "Export"
|
||||||
"title" = "Inbounds"
|
"title" = "Inbounds"
|
||||||
"totalDownUp" = "Total uploads/downloads"
|
"totalDownUp" = "Total Uploads/Downloads"
|
||||||
"totalUsage" = "Total usage"
|
"totalUsage" = "Total Usage"
|
||||||
"inboundCount" = "Number of inbound"
|
"inboundCount" = "Number Of Inbound"
|
||||||
"operate" = "Actions"
|
"operate" = "Actions"
|
||||||
"enable" = "Enable"
|
"enable" = "Enable"
|
||||||
"remark" = "Remark"
|
"remark" = "Remark"
|
||||||
|
@ -100,11 +102,11 @@
|
||||||
"traffic" = "Traffic"
|
"traffic" = "Traffic"
|
||||||
"details" = "Details"
|
"details" = "Details"
|
||||||
"transportConfig" = "Transport"
|
"transportConfig" = "Transport"
|
||||||
"expireDate" = "Expire date"
|
"expireDate" = "Expire Date"
|
||||||
"resetTraffic" = "Reset traffic"
|
"resetTraffic" = "Reset Traffic"
|
||||||
"addInbound" = "Add Inbound"
|
"addInbound" = "Add Inbound"
|
||||||
"addTo" = "Add To"
|
"addTo" = "Add To"
|
||||||
"revise" = "Revise"
|
"revise" = "Save"
|
||||||
"modifyInbound" = "Modify InBound"
|
"modifyInbound" = "Modify InBound"
|
||||||
"deleteInbound" = "Delete Inbound"
|
"deleteInbound" = "Delete Inbound"
|
||||||
"deleteInboundContent" = "Are you sure you want to delete inbound?"
|
"deleteInboundContent" = "Are you sure you want to delete inbound?"
|
||||||
|
@ -113,56 +115,44 @@
|
||||||
"address" = "Address"
|
"address" = "Address"
|
||||||
"network" = "Network"
|
"network" = "Network"
|
||||||
"destinationPort" = "Destination port"
|
"destinationPort" = "Destination port"
|
||||||
"targetAddress" = "Target address"
|
"targetAddress" = "Target Address"
|
||||||
"disableInsecureEncryption" = "Disable insecure encryption"
|
"disableInsecureEncryption" = "Disable insecure encryption"
|
||||||
"monitorDesc" = "Leave blank by default"
|
"monitorDesc" = "Leave blank by default"
|
||||||
"meansNoLimit" = "Means no limit"
|
"meansNoLimit" = "Means No Limit"
|
||||||
"totalFlow" = "Total flow"
|
"totalFlow" = "Total Traffic"
|
||||||
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
"leaveBlankToNeverExpire" = "Leave blank to never expire"
|
||||||
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
|
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
|
||||||
"certificatePath" = "Certificate file path"
|
"certificatePath" = "Certificate File Path"
|
||||||
"certificateContent" = "Certificate file content"
|
"certificateContent" = "Certificate File Content"
|
||||||
"publicKeyPath" = "Public key path"
|
"publicKeyPath" = "Public Key Path"
|
||||||
"publicKeyContent" = "Public key content"
|
"publicKeyContent" = "public Key Content"
|
||||||
"keyPath" = "Private Key path"
|
"keyPath" = "Private key Path"
|
||||||
"keyContent" = "Private Key content"
|
"keyContent" = "Private Key Content"
|
||||||
"clickOnQRcode" = "Click on QR Code to Copy"
|
|
||||||
"client" = "Client"
|
"client" = "Client"
|
||||||
"export" = "Export links"
|
"uid" = "UID"
|
||||||
|
|
||||||
[pages.client]
|
|
||||||
"add" = "Add client"
|
|
||||||
"edit" = "Edit client"
|
|
||||||
"submitAdd" = "Add client"
|
|
||||||
"submitEdit" = "Save changes"
|
|
||||||
"clientCount" = "Number of clients"
|
|
||||||
"bulk" = "Add bulk"
|
|
||||||
"method" = "Method"
|
|
||||||
"first" = "First"
|
|
||||||
"last" = "Last"
|
|
||||||
"prefix" = "Prefix"
|
|
||||||
"postfix" = "postfix"
|
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "Obtain"
|
"obtain" = "Obtain"
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"requestHeader" = "Request header"
|
"requestHeader" = "Request Header"
|
||||||
"name" = "Name"
|
"name" = "Name"
|
||||||
"value" = "Value"
|
"value" = "Value"
|
||||||
|
|
||||||
[pages.inbounds.stream.tcp]
|
[pages.inbounds.stream.tcp]
|
||||||
"requestVersion" = "Request version"
|
"requestVersion" = "Request Version"
|
||||||
"requestMethod" = "Request method"
|
"requestMethod" = "Request Method"
|
||||||
"requestPath" = "Request path"
|
"requestPath" = "Request Path"
|
||||||
"responseVersion" = "Response version"
|
"responseVersion" = "Response Version"
|
||||||
"responseStatus" = "Response status"
|
"responseStatus" = "Response Status"
|
||||||
"responseStatusDescription" = "Response status description"
|
"responseStatusDescription" = "Response Status Description"
|
||||||
"responseHeader" = "Response header"
|
"responseHeader" = "Response Header"
|
||||||
|
|
||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "Encryption"
|
"encryption" = "Encryption"
|
||||||
|
|
||||||
|
|
||||||
[pages.setting]
|
[pages.setting]
|
||||||
"title" = "Setting"
|
"title" = "Setting"
|
||||||
"save" = "Save"
|
"save" = "Save"
|
||||||
|
@ -170,7 +160,7 @@
|
||||||
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
|
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
|
||||||
"panelConfig" = "Panel Configuration"
|
"panelConfig" = "Panel Configuration"
|
||||||
"userSetting" = "User Setting"
|
"userSetting" = "User Setting"
|
||||||
"xrayConfiguration" = "xray Configuration"
|
"xrayConfiguration" = "Xray Configuration"
|
||||||
"TGReminder" = "TG Reminder Related Settings"
|
"TGReminder" = "TG Reminder Related Settings"
|
||||||
"otherSetting" = "Other Setting"
|
"otherSetting" = "Other Setting"
|
||||||
"panelListeningIP" = "Panel listening IP"
|
"panelListeningIP" = "Panel listening IP"
|
||||||
|
@ -187,42 +177,22 @@
|
||||||
"currentPassword" = "Current Password"
|
"currentPassword" = "Current Password"
|
||||||
"newUsername" = "New Username"
|
"newUsername" = "New Username"
|
||||||
"newPassword" = "New Password"
|
"newPassword" = "New Password"
|
||||||
"advancedTemplate" = "Advanced template parts"
|
"xrayConfigTemplate" = "xray Configuration Template"
|
||||||
"completeTemplate" = "Complete template of Xray configuration"
|
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect"
|
||||||
"xrayConfigTemplate" = "Xray Configuration Template"
|
|
||||||
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
|
|
||||||
"xrayConfigTorrent" = "Ban bittorrent usage"
|
|
||||||
"xrayConfigTorrentDesc" = "Change the configuration temlate to avoid using bittorrent by users, restart the panel to take effect"
|
|
||||||
"xrayConfigPrivateIp" = "Ban private ip range to connect"
|
|
||||||
"xrayConfigPrivateIpDesc" = "Change the configuration temlate to avoid connecting with private IP ranges, restart the panel to take effect"
|
|
||||||
"xrayConfigInbounds" = "Configuration of Inbounds"
|
|
||||||
"xrayConfigInboundsDesc" = "Change the configuration temlate to accept special clients, restart the panel to take effect"
|
|
||||||
"xrayConfigOutbounds" = "Configuration of Outbounds"
|
|
||||||
"xrayConfigOutboundsDesc" = "Change the configuration temlate to define outgoing ways for this server, restart the panel to take effect"
|
|
||||||
"xrayConfigRoutings" = "Configuration of Routing rules"
|
|
||||||
"xrayConfigRoutingsDesc" = "Change the configuration temlate to define Routing rules for this server, restart the panel to take effect"
|
|
||||||
"telegramBotEnable" = "Enable telegram bot"
|
"telegramBotEnable" = "Enable telegram bot"
|
||||||
"telegramBotEnableDesc" = "Restart the panel to take effect"
|
"telegramBotEnableDesc" = "Restart the panel to take effect"
|
||||||
"telegramToken" = "Telegram Token"
|
"telegramToken" = "Telegram Token"
|
||||||
"telegramTokenDesc" = "Restart the panel to take effect"
|
"telegramTokenDesc" = "Restart the panel to take effect"
|
||||||
"telegramChatId" = "Telegram Admin ChatIds"
|
"telegramChatId" = "Telegram ChatId"
|
||||||
"telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect"
|
"telegramChatIdDesc" = "Restart the panel to take effect"
|
||||||
"telegramNotifyTime" = "Telegram bot notification time"
|
"telegramNotifyTime" = "Telegram bot notification time"
|
||||||
"telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
|
"telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect"
|
||||||
"tgNotifyBackup" = "Database backup"
|
|
||||||
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
|
|
||||||
"tgNotifyExpireTimeDiff" = "Remained time threshold"
|
|
||||||
"tgNotifyExpireTimeDiffDesc" = "This telegram bot will send you a notification before expiration (unit:day)"
|
|
||||||
"tgNotifyTrafficDiff" = "Remained traffic threshold"
|
|
||||||
"tgNotifyTrafficDiffDesc" = "This telegram bot will send you a notification before finishing traffic (unit:GB)"
|
|
||||||
"tgNotifyCpu" = "CPU percentage alert threshold"
|
|
||||||
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
|
|
||||||
"timeZonee" = "Time Zone"
|
"timeZonee" = "Time Zone"
|
||||||
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
|
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
|
||||||
|
|
||||||
[pages.setting.toasts]
|
[pages.setting.toasts]
|
||||||
"modifySetting" = "Modify setting"
|
"modifySetting" = "modify setting"
|
||||||
"getSetting" = "Get setting"
|
"getSetting" = "get setting"
|
||||||
"modifyUser" = "Modify user"
|
"modifyUser" = "modify user"
|
||||||
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
|
"originalUserPassIncorrect" = "The original user name or original password is incorrect"
|
||||||
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
"userPassMustBeNotEmpty" = "New username and new password cannot be empty"
|
|
@ -10,8 +10,6 @@
|
||||||
"remark" = "نام"
|
"remark" = "نام"
|
||||||
"enable" = "فعال"
|
"enable" = "فعال"
|
||||||
"protocol" = "پروتکل"
|
"protocol" = "پروتکل"
|
||||||
"search" = "جستجو"
|
|
||||||
|
|
||||||
"loading" = "در حال بروزرسانی..."
|
"loading" = "در حال بروزرسانی..."
|
||||||
"second" = "ثانیه"
|
"second" = "ثانیه"
|
||||||
"minute" = "دقیقه"
|
"minute" = "دقیقه"
|
||||||
|
@ -22,7 +20,6 @@
|
||||||
"unlimited" = "نامحدود"
|
"unlimited" = "نامحدود"
|
||||||
"none" = "هیچ"
|
"none" = "هیچ"
|
||||||
"qrCode" = "QR کد"
|
"qrCode" = "QR کد"
|
||||||
"info" = "اطلاعات بیشتر"
|
|
||||||
"edit" = "ویرایش"
|
"edit" = "ویرایش"
|
||||||
"delete" = "حذف"
|
"delete" = "حذف"
|
||||||
"reset" = "ریست"
|
"reset" = "ریست"
|
||||||
|
@ -33,8 +30,8 @@
|
||||||
"host" = "آدرس"
|
"host" = "آدرس"
|
||||||
"path" = "مسیر"
|
"path" = "مسیر"
|
||||||
"camouflage" = "استتار"
|
"camouflage" = "استتار"
|
||||||
"enabled" = "فعال"
|
"enabled" = "فعال شد"
|
||||||
"disabled" = "غیرفعال"
|
"disabled" = "غیرفعال شد"
|
||||||
"domainName" = "آدرس دامنه"
|
"domainName" = "آدرس دامنه"
|
||||||
"additional" = "آی دی جایگزین"
|
"additional" = "آی دی جایگزین"
|
||||||
"monitor" = "آی پی اتصال"
|
"monitor" = "آی پی اتصال"
|
||||||
|
@ -43,8 +40,11 @@
|
||||||
"success" = " موفق"
|
"success" = " موفق"
|
||||||
"getVersion" = "دریافت ورژن"
|
"getVersion" = "دریافت ورژن"
|
||||||
"install" = "نصب"
|
"install" = "نصب"
|
||||||
|
"used" = "استفاده شده"
|
||||||
"clients" = "کاربران"
|
"clients" = "کاربران"
|
||||||
|
"search" = "جستجو"
|
||||||
"usage" = "استفاده"
|
"usage" = "استفاده"
|
||||||
|
"info" = "جزئیات"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "وضعیت سیستم"
|
"dashboard" = "وضعیت سیستم"
|
||||||
|
@ -64,19 +64,20 @@
|
||||||
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
|
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
|
||||||
"successLogin" = "خوش آمدید"
|
"successLogin" = "خوش آمدید"
|
||||||
|
|
||||||
|
|
||||||
[pages.index]
|
[pages.index]
|
||||||
"title" = "وضعیت سیستم"
|
"title" = "وضعیت سیستم"
|
||||||
"memory" = "حافظه رم"
|
"memory" = "حافظه رم"
|
||||||
"hard" = "حافظه دیسک"
|
"hard" = "حافظه دیسک"
|
||||||
"xrayStatus" = "وضعیت Xray"
|
"xrayStatus" = "وضعیت Xray"
|
||||||
"stopXray" = "توقف"
|
|
||||||
"restartXray" = "شروع مجدد"
|
|
||||||
"xraySwitch" = "تغییر ورژن"
|
"xraySwitch" = "تغییر ورژن"
|
||||||
|
"restartXray" = "راه اندازی مجدد"
|
||||||
|
"stopXray" = "توقف"
|
||||||
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
|
||||||
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
|
||||||
"operationHours" = "ساعت فعال"
|
"operationHours" = "ساعت فعال"
|
||||||
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
|
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
|
||||||
"systemLoad" = "بار روی سیستم"
|
"systemLoad" = "سرعت لود سیستم"
|
||||||
"connectionCount" = "تعداد کانکشن ها"
|
"connectionCount" = "تعداد کانکشن ها"
|
||||||
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
|
||||||
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
"upSpeed" = "سرعت آپلود در حال حاضر سیستم"
|
||||||
|
@ -87,7 +88,9 @@
|
||||||
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
|
||||||
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
|
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
|
||||||
|
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
|
"export" = "استخراج لینکها"
|
||||||
"title" = "کاربران"
|
"title" = "کاربران"
|
||||||
"totalDownUp" = "جمع آپلود/دانلود"
|
"totalDownUp" = "جمع آپلود/دانلود"
|
||||||
"totalUsage" = "جمع کل"
|
"totalUsage" = "جمع کل"
|
||||||
|
@ -104,7 +107,7 @@
|
||||||
"resetTraffic" = "ریست ترافیک"
|
"resetTraffic" = "ریست ترافیک"
|
||||||
"addInbound" = "اضافه کردن سرویس"
|
"addInbound" = "اضافه کردن سرویس"
|
||||||
"addTo" = "اضافه کردن"
|
"addTo" = "اضافه کردن"
|
||||||
"revise" = "ویرایش"
|
"revise" = "ذخیره"
|
||||||
"modifyInbound" = "ویرایش سرویس"
|
"modifyInbound" = "ویرایش سرویس"
|
||||||
"deleteInbound" = "حذف سرویس"
|
"deleteInbound" = "حذف سرویس"
|
||||||
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
|
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
|
||||||
|
@ -126,22 +129,8 @@
|
||||||
"publicKeyContent" = "محتوای Certificate.crt"
|
"publicKeyContent" = "محتوای Certificate.crt"
|
||||||
"keyPath" = "مسیر فایل Private.key"
|
"keyPath" = "مسیر فایل Private.key"
|
||||||
"keyContent" = "محتوای Private.key"
|
"keyContent" = "محتوای Private.key"
|
||||||
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
|
|
||||||
"client" = "کاربر"
|
"client" = "کاربر"
|
||||||
"export" = "استخراج لینکها"
|
"uid" = "UID"
|
||||||
|
|
||||||
[pages.client]
|
|
||||||
"add" = "کاربر جدید"
|
|
||||||
"edit" = "ویرایش کاربر"
|
|
||||||
"submitAdd" = "اضافه کردن"
|
|
||||||
"submitEdit" = "ذخیره تغییرات"
|
|
||||||
"clientCount" = "تعداد کاربران"
|
|
||||||
"bulk" = "انبوه سازی"
|
|
||||||
"method" = "روش"
|
|
||||||
"first" = "از"
|
|
||||||
"last" = "تا"
|
|
||||||
"prefix" = "پیشوند"
|
|
||||||
"postfix" = "پسوند"
|
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "Obtain"
|
"obtain" = "Obtain"
|
||||||
|
@ -163,6 +152,7 @@
|
||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "رمزنگاری"
|
"encryption" = "رمزنگاری"
|
||||||
|
|
||||||
|
|
||||||
[pages.setting]
|
[pages.setting]
|
||||||
"title" = "تنظیمات"
|
"title" = "تنظیمات"
|
||||||
"save" = "ذخیره"
|
"save" = "ذخیره"
|
||||||
|
@ -187,36 +177,16 @@
|
||||||
"currentPassword" = "رمز عبور فعلی"
|
"currentPassword" = "رمز عبور فعلی"
|
||||||
"newUsername" = "نام کاربری جدید"
|
"newUsername" = "نام کاربری جدید"
|
||||||
"newPassword" = "رمز عبور جدید"
|
"newPassword" = "رمز عبور جدید"
|
||||||
"advancedTemplate" = "بخش های پیشرفته الگو"
|
"xrayConfigTemplate" = "تنظیمات قالب Xray"
|
||||||
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
|
"xrayConfigTemplateDesc" = "فایل پیکربندی xray نهایی را بر اساس این الگو ایجاد کنید. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
|
|
||||||
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
|
|
||||||
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
|
|
||||||
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigInbounds" = "تنظیمات ورودی"
|
|
||||||
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigOutbounds" = "تنظیمات خروجی"
|
|
||||||
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
|
|
||||||
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
|
||||||
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
"telegramBotEnable" = "فعالسازی ربات تلگرام"
|
||||||
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"telegramToken" = "توکن تلگرام"
|
"telegramToken" = "توکن تلگرام"
|
||||||
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"telegramChatId" = "آی دی تلگرام مدیریت"
|
"telegramChatId" = "آی دی تلگرام مدیریت . از ربات @getidsbot آی دی خود را دریافت کنید"
|
||||||
"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramChatIdDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
|
||||||
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
|
||||||
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
|
|
||||||
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
|
|
||||||
"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
|
|
||||||
"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
|
|
||||||
"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
|
|
||||||
"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
|
|
||||||
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
|
|
||||||
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
|
|
||||||
"timeZonee" = "منظقه زمانی"
|
"timeZonee" = "منظقه زمانی"
|
||||||
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
|
||||||
|
|
||||||
|
|
|
@ -10,8 +10,6 @@
|
||||||
"remark" = "备注"
|
"remark" = "备注"
|
||||||
"enable" = "启用"
|
"enable" = "启用"
|
||||||
"protocol" = "协议"
|
"protocol" = "协议"
|
||||||
"search" = "搜尋"
|
|
||||||
|
|
||||||
"loading" = "加载中"
|
"loading" = "加载中"
|
||||||
"second" = "秒"
|
"second" = "秒"
|
||||||
"minute" = "分钟"
|
"minute" = "分钟"
|
||||||
|
@ -22,7 +20,6 @@
|
||||||
"unlimited" = "无限制"
|
"unlimited" = "无限制"
|
||||||
"none" = "无"
|
"none" = "无"
|
||||||
"qrCode" = "二维码"
|
"qrCode" = "二维码"
|
||||||
"info" = "更多信息"
|
|
||||||
"edit" = "编辑"
|
"edit" = "编辑"
|
||||||
"delete" = "删除"
|
"delete" = "删除"
|
||||||
"reset" = "重置"
|
"reset" = "重置"
|
||||||
|
@ -43,8 +40,11 @@
|
||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
"getVersion" = "获取版本"
|
"getVersion" = "获取版本"
|
||||||
"install" = "安装"
|
"install" = "安装"
|
||||||
|
"used" = "用过的"
|
||||||
"clients" = "客户端"
|
"clients" = "客户端"
|
||||||
|
"search" = "搜索"
|
||||||
"usage" = "用法"
|
"usage" = "用法"
|
||||||
|
"info" = "细节"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"dashboard" = "系统状态"
|
"dashboard" = "系统状态"
|
||||||
|
@ -69,9 +69,9 @@
|
||||||
"memory" = "内存"
|
"memory" = "内存"
|
||||||
"hard" = "硬盘"
|
"hard" = "硬盘"
|
||||||
"xrayStatus" = "xray 状态"
|
"xrayStatus" = "xray 状态"
|
||||||
"stopXray" = "停止 Xray"
|
|
||||||
"restartXray" = "重启 Xray"
|
|
||||||
"xraySwitch" = "切换版本"
|
"xraySwitch" = "切换版本"
|
||||||
|
"restartXray" = "重新开始"
|
||||||
|
"stopXray" = "停止"
|
||||||
"xraySwitchClick" = "点击你想切换的版本"
|
"xraySwitchClick" = "点击你想切换的版本"
|
||||||
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
|
||||||
"operationHours" = "运行时间"
|
"operationHours" = "运行时间"
|
||||||
|
@ -87,7 +87,9 @@
|
||||||
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
|
||||||
"dontRefreshh" = "安装中,请不要刷新此页面"
|
"dontRefreshh" = "安装中,请不要刷新此页面"
|
||||||
|
|
||||||
|
|
||||||
[pages.inbounds]
|
[pages.inbounds]
|
||||||
|
"export" = "导出链接"
|
||||||
"title" = "入站列表"
|
"title" = "入站列表"
|
||||||
"totalDownUp" = "总上传 / 下载"
|
"totalDownUp" = "总上传 / 下载"
|
||||||
"totalUsage" = "总用量"
|
"totalUsage" = "总用量"
|
||||||
|
@ -126,22 +128,9 @@
|
||||||
"publicKeyContent" = "公钥内容"
|
"publicKeyContent" = "公钥内容"
|
||||||
"keyPath" = "密钥文件路径"
|
"keyPath" = "密钥文件路径"
|
||||||
"keyContent" = "密钥内容"
|
"keyContent" = "密钥内容"
|
||||||
"clickOnQRcode" = "点击二维码复制"
|
|
||||||
"client" = "客户"
|
"client" = "客户"
|
||||||
"export" = "导出链接"
|
"uid" = "UID"
|
||||||
|
|
||||||
[pages.client]
|
|
||||||
"add" = "添加客户端"
|
|
||||||
"edit" = "编辑客户"
|
|
||||||
"submitAdd" = "添加客户端"
|
|
||||||
"submitEdit" = "保存修改"
|
|
||||||
"clientCount" = "客户数量"
|
|
||||||
"bulk" = "批量创建"
|
|
||||||
"method" = "方法"
|
|
||||||
"first" = "第一"
|
|
||||||
"last" = "最后"
|
|
||||||
"prefix" = "前缀"
|
|
||||||
"postfix" = "后缀"
|
|
||||||
|
|
||||||
[pages.inbounds.toasts]
|
[pages.inbounds.toasts]
|
||||||
"obtain" = "获取"
|
"obtain" = "获取"
|
||||||
|
@ -163,6 +152,7 @@
|
||||||
[pages.inbounds.stream.quic]
|
[pages.inbounds.stream.quic]
|
||||||
"encryption" = "加密"
|
"encryption" = "加密"
|
||||||
|
|
||||||
|
|
||||||
[pages.setting]
|
[pages.setting]
|
||||||
"title" = "设置"
|
"title" = "设置"
|
||||||
"save" = "保存配置"
|
"save" = "保存配置"
|
||||||
|
@ -187,36 +177,16 @@
|
||||||
"currentPassword" = "原密码"
|
"currentPassword" = "原密码"
|
||||||
"newUsername" = "新用户名"
|
"newUsername" = "新用户名"
|
||||||
"newPassword" = "新密码"
|
"newPassword" = "新密码"
|
||||||
"advancedTemplate" = "高级模板部件"
|
"xrayConfigTemplate" = "xray 配置模版"
|
||||||
"completeTemplate" = "Xray 配置的完整模板"
|
"xrayConfigTemplateDesc" = "以该模版为基础生成最终的 xray 配置文件,重启面板生效"
|
||||||
"xrayConfigTemplate" = "xray 配置模板"
|
|
||||||
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件,重新启动面板生成效率"
|
|
||||||
"xrayConfigTorrent" = "禁止使用 bittorrent"
|
|
||||||
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent,重启面板生效"
|
|
||||||
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
|
|
||||||
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
|
|
||||||
"xrayConfigInbounds" = "入站配置"
|
|
||||||
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
|
|
||||||
"xrayConfigOutbounds" = "出站配置"
|
|
||||||
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
|
|
||||||
"xrayConfigRoutings" = "路由规则配置"
|
|
||||||
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
|
|
||||||
"telegramBotEnable" = "启用电报机器人"
|
"telegramBotEnable" = "启用电报机器人"
|
||||||
"telegramBotEnableDesc" = "重启面板生效"
|
"telegramBotEnableDesc" = "重启面板生效"
|
||||||
"telegramToken" = "电报机器人TOKEN"
|
"telegramToken" = "电报机器人TOKEN"
|
||||||
"telegramTokenDesc" = "重启面板生效"
|
"telegramTokenDesc" = "重启面板生效"
|
||||||
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
|
"telegramChatId" = "电报机器人ChatId"
|
||||||
"telegramChatIdDesc" = "重启面板生效"
|
"telegramChatIdDesc" = "重启面板生效"
|
||||||
"telegramNotifyTime" = "电报机器人通知时间"
|
"telegramNotifyTime" = "电报机器人通知时间"
|
||||||
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
|
||||||
"tgNotifyBackup" = "数据库备份"
|
|
||||||
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
|
|
||||||
"tgNotifyExpireTimeDiff" = "剩余时间阈值"
|
|
||||||
"tgNotifyExpireTimeDiffDesc" = "这个 talegram bot 会在到期前给你发送通知(单位:天)"
|
|
||||||
"tgNotifyTrafficDiff" = "剩余流量阈值"
|
|
||||||
"tgNotifyTrafficDiffDesc" = "这个 talegram bot 会在流量结束前给你发送通知(单位:GB)"
|
|
||||||
"tgNotifyCpu" = "CPU 百分比警报阈值"
|
|
||||||
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
|
|
||||||
"timeZonee" = "时区"
|
"timeZonee" = "时区"
|
||||||
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
|
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
|
||||||
|
|
||||||
|
|
22
web/web.go
|
@ -21,11 +21,11 @@ import (
|
||||||
"x-ui/web/network"
|
"x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml/v2"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-contrib/sessions/cookie"
|
"github.com/gin-contrib/sessions/cookie"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
"github.com/pelletier/go-toml/v2"
|
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
@ -88,7 +88,7 @@ type Server struct {
|
||||||
|
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
tgbotService service.Tgbot
|
inboundService service.InboundService
|
||||||
|
|
||||||
cron *cron.Cron
|
cron *cron.Cron
|
||||||
|
|
||||||
|
@ -328,13 +328,8 @@ func (s *Server) startTask() {
|
||||||
logger.Warning("Add NewStatsNotifyJob error", err)
|
logger.Warning("Add NewStatsNotifyJob error", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// listen for TG bot income messages
|
||||||
// Check CPU load and alarm to TgBot if threshold passes
|
go job.NewStatsNotifyJob().OnReceive()
|
||||||
cpuThreshold, err := s.settingService.GetTgCpu()
|
|
||||||
if (err == nil) && (cpuThreshold > 0) {
|
|
||||||
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
s.cron.Remove(entry)
|
s.cron.Remove(entry)
|
||||||
}
|
}
|
||||||
|
@ -411,12 +406,6 @@ func (s *Server) Start() (err error) {
|
||||||
s.httpServer.Serve(listener)
|
s.httpServer.Serve(listener)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
isTgbotenabled, err := s.settingService.GetTgbotenabled()
|
|
||||||
if (err == nil) && (isTgbotenabled) {
|
|
||||||
tgBot := s.tgbotService.NewTgbot()
|
|
||||||
tgBot.Start()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -426,9 +415,6 @@ func (s *Server) Stop() error {
|
||||||
if s.cron != nil {
|
if s.cron != nil {
|
||||||
s.cron.Stop()
|
s.cron.Stop()
|
||||||
}
|
}
|
||||||
if s.tgbotService.IsRunnging() {
|
|
||||||
s.tgbotService.Stop()
|
|
||||||
}
|
|
||||||
var err1 error
|
var err1 error
|
||||||
var err2 error
|
var err2 error
|
||||||
if s.httpServer != nil {
|
if s.httpServer != nil {
|
||||||
|
|