mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-20 17:45:49 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5408a2f82c | ||
|
|
c8d71ea748 | ||
|
|
46de886b53 | ||
|
|
6d41320ed7 | ||
|
|
bf9d2e6aeb | ||
|
|
ed96fa090b | ||
|
|
3ac1d7f546 | ||
|
|
10025ffa66 | ||
|
|
5ee62b25ca | ||
|
|
311d11a3c1 | ||
|
|
40b6d7707a | ||
|
|
cbf316db31 | ||
|
|
33a36ada4b | ||
|
|
82ddd10627 | ||
|
|
2401c99817 | ||
|
|
2f36a4047c | ||
|
|
dc3b0d218a | ||
|
|
610d29765a | ||
|
|
b1ea8005e4 | ||
|
|
3f0bfa2472 | ||
|
|
1e2ff650ad | ||
|
|
c2d6dd923f | ||
|
|
723ec25fb2 | ||
|
|
7dc52e9a53 | ||
|
|
fe9f0d1d0e | ||
|
|
18d74d54ca | ||
|
|
c7ba6ae909 | ||
|
|
3edf79e589 | ||
|
|
5420e643cf | ||
|
|
9fcd0387ca | ||
|
|
7b039d219e | ||
|
|
dbec28b915 | ||
|
|
e5126806d7 | ||
|
|
b008ff4ad2 | ||
|
|
da6b89fdcd | ||
|
|
d7882c25d1 | ||
|
|
ed2a0a0bcf | ||
|
|
4a0914cb1e | ||
|
|
664269d513 | ||
|
|
d0796b26c9 |
88
.github/workflows/release.yml
vendored
88
.github/workflows/release.yml
vendored
@@ -7,8 +7,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
tags:
|
||||||
|
- "v*.*.*"
|
||||||
paths:
|
paths:
|
||||||
- '.github/workflows/release.yml'
|
|
||||||
- '**.js'
|
- '**.js'
|
||||||
- '**.css'
|
- '**.css'
|
||||||
- '**.html'
|
- '**.html'
|
||||||
@@ -38,7 +39,7 @@ jobs:
|
|||||||
uses: actions/checkout@v5
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v6
|
||||||
with:
|
with:
|
||||||
go-version-file: go.mod
|
go-version-file: go.mod
|
||||||
check-latest: true
|
check-latest: true
|
||||||
@@ -84,7 +85,7 @@ jobs:
|
|||||||
cd x-ui/bin
|
cd x-ui/bin
|
||||||
|
|
||||||
# Download dependencies
|
# Download dependencies
|
||||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.8.29/"
|
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
||||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
wget -q ${Xray_URL}Xray-linux-64.zip
|
||||||
unzip Xray-linux-64.zip
|
unzip Xray-linux-64.zip
|
||||||
@@ -135,10 +136,89 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload files to GH release
|
- name: Upload files to GH release
|
||||||
uses: svenstaro/upload-release-action@v2
|
uses: svenstaro/upload-release-action@v2
|
||||||
if: github.event_name == 'release' && github.event.action == 'published'
|
if: |
|
||||||
|
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||||
|
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
with:
|
with:
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag: ${{ github.ref }}
|
tag: ${{ github.ref }}
|
||||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
|
overwrite: true
|
||||||
|
prerelease: true
|
||||||
|
|
||||||
|
# =================================
|
||||||
|
# Windows Build
|
||||||
|
# =================================
|
||||||
|
build-windows:
|
||||||
|
name: Build for Windows
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
platform:
|
||||||
|
- amd64
|
||||||
|
runs-on: windows-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v5
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version-file: go.mod
|
||||||
|
check-latest: true
|
||||||
|
|
||||||
|
- name: Build 3X-UI for Windows
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$env:CGO_ENABLED="1"
|
||||||
|
$env:GOOS="windows"
|
||||||
|
$env:GOARCH="amd64"
|
||||||
|
go build -ldflags "-w -s" -o xui-release.exe -v main.go
|
||||||
|
|
||||||
|
mkdir x-ui
|
||||||
|
Copy-Item xui-release.exe x-ui\
|
||||||
|
mkdir x-ui\bin
|
||||||
|
cd x-ui\bin
|
||||||
|
|
||||||
|
# Download Xray for Windows
|
||||||
|
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
||||||
|
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
||||||
|
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
||||||
|
Remove-Item "Xray-windows-64.zip"
|
||||||
|
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
|
||||||
|
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
|
||||||
|
Rename-Item xray.exe xray-windows-amd64.exe
|
||||||
|
cd ..
|
||||||
|
Copy-Item -Path ..\windows_files\* -Destination . -Recurse
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Package to Zip
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
|
||||||
|
|
||||||
|
- name: Upload files to Artifacts
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: x-ui-windows-amd64
|
||||||
|
path: ./x-ui-windows-amd64.zip
|
||||||
|
|
||||||
|
- name: Upload files to GH release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||||
|
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
file: x-ui-windows-amd64.zip
|
||||||
|
asset_name: x-ui-windows-amd64.zip
|
||||||
|
overwrite: true
|
||||||
prerelease: true
|
prerelease: true
|
||||||
@@ -27,7 +27,7 @@ case $1 in
|
|||||||
esac
|
esac
|
||||||
mkdir -p build/bin
|
mkdir -p build/bin
|
||||||
cd build/bin
|
cd build/bin
|
||||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.8.29/Xray-linux-${ARCH}.zip"
|
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/Xray-linux-${ARCH}.zip"
|
||||||
unzip "Xray-linux-${ARCH}.zip"
|
unzip "Xray-linux-${ARCH}.zip"
|
||||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||||
mv xray "xray-linux-${FNAME}"
|
mv xray "xray-linux-${FNAME}"
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func GetLogFolder() string {
|
|||||||
return logFolderPath
|
return logFolderPath
|
||||||
}
|
}
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
return getBaseDir()
|
return filepath.Join(".", "log")
|
||||||
}
|
}
|
||||||
return "/var/log"
|
return "/var/log"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.6.7
|
2.8.0
|
||||||
@@ -12,11 +12,11 @@ type Protocol string
|
|||||||
const (
|
const (
|
||||||
VMESS Protocol = "vmess"
|
VMESS Protocol = "vmess"
|
||||||
VLESS Protocol = "vless"
|
VLESS Protocol = "vless"
|
||||||
DOKODEMO Protocol = "dokodemo-door"
|
Tunnel Protocol = "tunnel"
|
||||||
HTTP Protocol = "http"
|
HTTP Protocol = "http"
|
||||||
Trojan Protocol = "trojan"
|
Trojan Protocol = "trojan"
|
||||||
Shadowsocks Protocol = "shadowsocks"
|
Shadowsocks Protocol = "shadowsocks"
|
||||||
Socks Protocol = "socks"
|
Mixed Protocol = "mixed"
|
||||||
WireGuard Protocol = "wireguard"
|
WireGuard Protocol = "wireguard"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,3 +106,10 @@ type Client struct {
|
|||||||
CreatedAt int64 `json:"created_at,omitempty"`
|
CreatedAt int64 `json:"created_at,omitempty"`
|
||||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type VLESSSettings struct {
|
||||||
|
Clients []Client `json:"clients"`
|
||||||
|
Decryption string `json:"decryption"`
|
||||||
|
Encryption string `json:"encryption"`
|
||||||
|
Fallbacks []any `json:"fallbacks"`
|
||||||
|
}
|
||||||
|
|||||||
40
go.mod
40
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module x-ui
|
module x-ui
|
||||||
|
|
||||||
go 1.25.0
|
go 1.25.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-contrib/gzip v1.2.3
|
github.com/gin-contrib/gzip v1.2.3
|
||||||
@@ -14,21 +14,23 @@ require (
|
|||||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4
|
github.com/pelletier/go-toml/v2 v2.2.4
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/shirou/gopsutil/v4 v4.25.7
|
github.com/shirou/gopsutil/v4 v4.25.8
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||||
github.com/valyala/fasthttp v1.65.0
|
github.com/valyala/fasthttp v1.65.0
|
||||||
github.com/xlzd/gotp v0.1.0
|
github.com/xlzd/gotp v0.1.0
|
||||||
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5
|
github.com/xtls/xray-core v1.250911.0
|
||||||
go.uber.org/atomic v1.11.0
|
go.uber.org/atomic v1.11.0
|
||||||
golang.org/x/crypto v0.41.0
|
golang.org/x/crypto v0.42.0
|
||||||
golang.org/x/text v0.28.0
|
golang.org/x/text v0.29.0
|
||||||
google.golang.org/grpc v1.75.0
|
google.golang.org/grpc v1.75.1
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.30.2
|
gorm.io/gorm v1.30.5
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/bytedance/sonic v1.14.0 // indirect
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.14.1 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||||
github.com/cloudflare/circl v1.6.1 // indirect
|
github.com/cloudflare/circl v1.6.1 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
@@ -67,8 +69,8 @@ require (
|
|||||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||||
github.com/sagernet/sing v0.7.5 // indirect
|
github.com/sagernet/sing v0.7.7 // indirect
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
|
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||||
@@ -79,21 +81,21 @@ require (
|
|||||||
github.com/valyala/fastjson v1.6.4 // indirect
|
github.com/valyala/fastjson v1.6.4 // indirect
|
||||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.5 // indirect
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f // indirect
|
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.uber.org/mock v0.6.0 // indirect
|
go.uber.org/mock v0.6.0 // indirect
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
golang.org/x/arch v0.20.0 // indirect
|
golang.org/x/arch v0.21.0 // indirect
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.28.0 // indirect
|
||||||
golang.org/x/net v0.43.0 // indirect
|
golang.org/x/net v0.44.0 // indirect
|
||||||
golang.org/x/sync v0.16.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.36.0 // indirect
|
||||||
golang.org/x/time v0.12.0 // indirect
|
golang.org/x/time v0.13.0 // indirect
|
||||||
golang.org/x/tools v0.36.0 // indirect
|
golang.org/x/tools v0.36.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
||||||
google.golang.org/protobuf v1.36.8 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
|
|||||||
76
go.sum
76
go.sum
@@ -2,8 +2,10 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
|
|||||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||||
|
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||||
@@ -132,14 +134,16 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||||
github.com/sagernet/sing v0.7.5 h1:gNMwZCLPqR+4e0g6dwi0sSsrvOmoMjpZgqxKsuJZatc=
|
github.com/sagernet/sing v0.7.7 h1:o46FzVZS+wKbBMEkMEdEHoVZxyM9jvfRpKXc7pEgS/c=
|
||||||
github.com/sagernet/sing v0.7.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
github.com/sagernet/sing v0.7.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
@@ -172,10 +176,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
|||||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||||
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f h1:o1Kryl9qEYYzNep9RId9DM1kBn8tBrcK5UJnti/l0NI=
|
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
|
||||||
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||||
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5 h1:rBqCVgic8yIUVHB4h26K8JNuwJuNj45egsdXxwEvA7E=
|
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
|
||||||
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5/go.mod h1:WB/73DmN9Vs7lxtx4Xc/D0Ub1VUu06hAh1mMh8JN2uM=
|
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
|
||||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
@@ -198,28 +202,28 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||||
@@ -228,12 +232,12 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
|
|||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1 h1:pmJpJEvT846VzausCQ5d7KreSROcDqmO388w5YbnltA=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250826171959-ef028d996bc1/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
@@ -245,8 +249,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/gorm v1.30.2 h1:f7bevlVoVe4Byu3pmbWPVHnPsLoWaMjEb7/clyr9Ivs=
|
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
|
||||||
gorm.io/gorm v1.30.2/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"inbounds": [
|
"inbounds": [
|
||||||
{
|
{
|
||||||
"port": 10808,
|
"port": 10808,
|
||||||
"protocol": "socks",
|
"protocol": "mixed",
|
||||||
"settings": {
|
"settings": {
|
||||||
"auth": "noauth",
|
"auth": "noauth",
|
||||||
"udp": true,
|
"udp": true,
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
],
|
],
|
||||||
"enabled": true
|
"enabled": true
|
||||||
},
|
},
|
||||||
"tag": "socks"
|
"tag": "mixed"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"port": 10809,
|
"port": 10809,
|
||||||
|
|||||||
92
sub/sub.go
92
sub/sub.go
@@ -3,14 +3,19 @@ package sub
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"io/fs"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"x-ui/config"
|
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
"x-ui/util/common"
|
"x-ui/util/common"
|
||||||
|
webpkg "x-ui/web"
|
||||||
|
"x-ui/web/locale"
|
||||||
"x-ui/web/middleware"
|
"x-ui/web/middleware"
|
||||||
"x-ui/web/network"
|
"x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
@@ -18,6 +23,21 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
||||||
|
func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||||
|
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
||||||
|
webpkg.EmbeddedHTML(),
|
||||||
|
"html/common/page.html",
|
||||||
|
"html/component/aThemeSwitch.html",
|
||||||
|
"html/subscription.html",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
engine.SetHTMLTemplate(t)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
@@ -38,13 +58,10 @@ func NewServer() *Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
if config.IsDebug() {
|
// Always run in release mode for the subscription server
|
||||||
gin.SetMode(gin.DebugMode)
|
gin.DefaultWriter = io.Discard
|
||||||
} else {
|
gin.DefaultErrorWriter = io.Discard
|
||||||
gin.DefaultWriter = io.Discard
|
gin.SetMode(gin.ReleaseMode)
|
||||||
gin.DefaultErrorWriter = io.Discard
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
|
||||||
}
|
|
||||||
|
|
||||||
engine := gin.Default()
|
engine := gin.Default()
|
||||||
|
|
||||||
@@ -57,6 +74,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Provide base_path in context for templates
|
||||||
|
engine.Use(func(c *gin.Context) {
|
||||||
|
c.Set("base_path", "/")
|
||||||
|
})
|
||||||
|
|
||||||
LinksPath, err := s.settingService.GetSubPath()
|
LinksPath, err := s.settingService.GetSubPath()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -112,6 +134,36 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
SubTitle = ""
|
SubTitle = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set per-request localizer from headers/cookies
|
||||||
|
engine.Use(locale.LocalizerMiddleware())
|
||||||
|
|
||||||
|
// register i18n function similar to web server
|
||||||
|
i18nWebFunc := func(key string, params ...string) string {
|
||||||
|
return locale.I18n(locale.Web, key, params...)
|
||||||
|
}
|
||||||
|
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
|
||||||
|
|
||||||
|
// Templates: prefer embedded; fallback to disk if necessary
|
||||||
|
if err := setEmbeddedTemplates(engine); err != nil {
|
||||||
|
logger.Warning("sub: failed to parse embedded templates:", err)
|
||||||
|
if files, derr := s.getHtmlFiles(); derr == nil {
|
||||||
|
engine.LoadHTMLFiles(files...)
|
||||||
|
} else {
|
||||||
|
logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assets: use disk if present, fallback to embedded
|
||||||
|
if _, err := os.Stat("web/assets"); err == nil {
|
||||||
|
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
||||||
|
} else {
|
||||||
|
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||||
|
engine.StaticFS("/assets", http.FS(subFS))
|
||||||
|
} else {
|
||||||
|
logger.Error("sub: failed to mount embedded assets:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
g := engine.Group("/")
|
g := engine.Group("/")
|
||||||
|
|
||||||
s.sub = NewSUBController(
|
s.sub = NewSUBController(
|
||||||
@@ -121,6 +173,30 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getHtmlFiles loads templates from local folder (used in debug mode)
|
||||||
|
func (s *Server) getHtmlFiles() ([]string, error) {
|
||||||
|
dir, _ := os.Getwd()
|
||||||
|
files := []string{}
|
||||||
|
// common layout
|
||||||
|
common := filepath.Join(dir, "web", "html", "common", "page.html")
|
||||||
|
if _, err := os.Stat(common); err == nil {
|
||||||
|
files = append(files, common)
|
||||||
|
}
|
||||||
|
// components used
|
||||||
|
theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
|
||||||
|
if _, err := os.Stat(theme); err == nil {
|
||||||
|
files = append(files, theme)
|
||||||
|
}
|
||||||
|
// page itself
|
||||||
|
page := filepath.Join(dir, "web", "html", "subscription.html")
|
||||||
|
if _, err := os.Stat(page); err == nil {
|
||||||
|
files = append(files, page)
|
||||||
|
} else {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) Start() (err error) {
|
func (s *Server) Start() (err error) {
|
||||||
// This is an anonymous function, no function name
|
// This is an anonymous function, no function name
|
||||||
defer func() {
|
defer func() {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package sub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"net"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"x-ui/config"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -53,27 +53,13 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
|||||||
gJson := g.Group(a.subJsonPath)
|
gJson := g.Group(a.subJsonPath)
|
||||||
|
|
||||||
gLink.GET(":subid", a.subs)
|
gLink.GET(":subid", a.subs)
|
||||||
|
|
||||||
gJson.GET(":subid", a.subJsons)
|
gJson.GET(":subid", a.subJsons)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SUBController) subs(c *gin.Context) {
|
func (a *SUBController) subs(c *gin.Context) {
|
||||||
subId := c.Param("subid")
|
subId := c.Param("subid")
|
||||||
var host string
|
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
||||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
subs, header, lastOnline, err := a.subService.GetSubs(subId, host)
|
||||||
host = h
|
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
host = c.GetHeader("X-Real-IP")
|
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
var err error
|
|
||||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
|
||||||
if err != nil {
|
|
||||||
host = c.Request.Host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
subs, header, err := a.subService.GetSubs(subId, host)
|
|
||||||
if err != nil || len(subs) == 0 {
|
if err != nil || len(subs) == 0 {
|
||||||
c.String(400, "Error!")
|
c.String(400, "Error!")
|
||||||
} else {
|
} else {
|
||||||
@@ -82,10 +68,38 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||||||
result += sub + "\n"
|
result += sub + "\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
|
||||||
|
accept := c.GetHeader("Accept")
|
||||||
|
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||||
|
// Build page data in service
|
||||||
|
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
||||||
|
page := a.subService.BuildPageData(subId, hostHeader, header, lastOnline, subs, subURL, subJsonURL)
|
||||||
|
c.HTML(200, "subscription.html", gin.H{
|
||||||
|
"title": "subscription.title",
|
||||||
|
"cur_ver": config.GetVersion(),
|
||||||
|
"host": page.Host,
|
||||||
|
"base_path": page.BasePath,
|
||||||
|
"sId": page.SId,
|
||||||
|
"download": page.Download,
|
||||||
|
"upload": page.Upload,
|
||||||
|
"total": page.Total,
|
||||||
|
"used": page.Used,
|
||||||
|
"remained": page.Remained,
|
||||||
|
"expire": page.Expire,
|
||||||
|
"lastOnline": page.LastOnline,
|
||||||
|
"datepicker": page.Datepicker,
|
||||||
|
"downloadByte": page.DownloadByte,
|
||||||
|
"uploadByte": page.UploadByte,
|
||||||
|
"totalByte": page.TotalByte,
|
||||||
|
"subUrl": page.SubUrl,
|
||||||
|
"subJsonUrl": page.SubJsonUrl,
|
||||||
|
"result": page.Result,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Add headers
|
// Add headers
|
||||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
|
||||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
|
||||||
|
|
||||||
if a.subEncrypt {
|
if a.subEncrypt {
|
||||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||||
@@ -97,41 +111,21 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||||||
|
|
||||||
func (a *SUBController) subJsons(c *gin.Context) {
|
func (a *SUBController) subJsons(c *gin.Context) {
|
||||||
subId := c.Param("subid")
|
subId := c.Param("subid")
|
||||||
var host string
|
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
|
||||||
host = h
|
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
host = c.GetHeader("X-Real-IP")
|
|
||||||
}
|
|
||||||
if host == "" {
|
|
||||||
var err error
|
|
||||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
|
||||||
if err != nil {
|
|
||||||
host = c.Request.Host
|
|
||||||
}
|
|
||||||
}
|
|
||||||
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
|
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
|
||||||
if err != nil || len(jsonSub) == 0 {
|
if err != nil || len(jsonSub) == 0 {
|
||||||
c.String(400, "Error!")
|
c.String(400, "Error!")
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
// Add headers
|
// Add headers
|
||||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
|
||||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
|
||||||
|
|
||||||
c.String(200, jsonSub)
|
c.String(200, jsonSub)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHostFromXFH(s string) (string, error) {
|
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||||
if strings.Contains(s, ":") {
|
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||||
realHost, _, err := net.SplitHostPort(s)
|
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||||
if err != nil {
|
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return realHost, nil
|
|
||||||
}
|
|
||||||
return s, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,8 +184,14 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
|||||||
var newOutbounds []json_util.RawMessage
|
var newOutbounds []json_util.RawMessage
|
||||||
|
|
||||||
switch inbound.Protocol {
|
switch inbound.Protocol {
|
||||||
case "vmess", "vless":
|
case "vmess":
|
||||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, ""))
|
||||||
|
case "vless":
|
||||||
|
var vlessSettings model.VLESSSettings
|
||||||
|
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
||||||
|
|
||||||
|
newOutbounds = append(newOutbounds,
|
||||||
|
s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption))
|
||||||
case "trojan", "shadowsocks":
|
case "trojan", "shadowsocks":
|
||||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||||
}
|
}
|
||||||
@@ -284,7 +290,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
|||||||
return rltyData
|
return rltyData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage {
|
||||||
outbound := Outbound{}
|
outbound := Outbound{}
|
||||||
usersData := make([]UserVnext, 1)
|
usersData := make([]UserVnext, 1)
|
||||||
|
|
||||||
@@ -295,7 +301,7 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
|||||||
}
|
}
|
||||||
if inbound.Protocol == model.VLESS {
|
if inbound.Protocol == model.VLESS {
|
||||||
usersData[0].Flow = client.Flow
|
usersData[0].Flow = client.Flow
|
||||||
usersData[0].Encryption = "none"
|
usersData[0].Encryption = encryption
|
||||||
}
|
}
|
||||||
|
|
||||||
vnextData := make([]VnextSetting, 1)
|
vnextData := make([]VnextSetting, 1)
|
||||||
|
|||||||
@@ -3,10 +3,15 @@ package sub
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/goccy/go-json"
|
||||||
|
|
||||||
"x-ui/database"
|
"x-ui/database"
|
||||||
"x-ui/database/model"
|
"x-ui/database/model"
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
@@ -14,8 +19,6 @@ import (
|
|||||||
"x-ui/util/random"
|
"x-ui/util/random"
|
||||||
"x-ui/web/service"
|
"x-ui/web/service"
|
||||||
"x-ui/xray"
|
"x-ui/xray"
|
||||||
|
|
||||||
"github.com/goccy/go-json"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubService struct {
|
type SubService struct {
|
||||||
@@ -34,19 +37,20 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
|
func (s *SubService) GetSubs(subId string, host string) ([]string, string, int64, error) {
|
||||||
s.address = host
|
s.address = host
|
||||||
var result []string
|
var result []string
|
||||||
var header string
|
var header string
|
||||||
var traffic xray.ClientTraffic
|
var traffic xray.ClientTraffic
|
||||||
|
var lastOnline int64
|
||||||
var clientTraffics []xray.ClientTraffic
|
var clientTraffics []xray.ClientTraffic
|
||||||
inbounds, err := s.getInboundsBySubId(subId)
|
inbounds, err := s.getInboundsBySubId(subId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", err
|
return nil, "", 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(inbounds) == 0 {
|
if len(inbounds) == 0 {
|
||||||
return nil, "", common.NewError("No inbounds found with ", subId)
|
return nil, "", 0, common.NewError("No inbounds found with ", subId)
|
||||||
}
|
}
|
||||||
|
|
||||||
s.datepicker, err = s.settingService.GetDatepicker()
|
s.datepicker, err = s.settingService.GetDatepicker()
|
||||||
@@ -73,7 +77,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
|||||||
if client.Enable && client.SubID == subId {
|
if client.Enable && client.SubID == subId {
|
||||||
link := s.getLink(inbound, client.Email)
|
link := s.getLink(inbound, client.Email)
|
||||||
result = append(result, link)
|
result = append(result, link)
|
||||||
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
|
ct := s.getClientTraffics(inbound.ClientStats, client.Email)
|
||||||
|
clientTraffics = append(clientTraffics, ct)
|
||||||
|
if ct.LastOnline > lastOnline {
|
||||||
|
lastOnline = ct.LastOnline
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +109,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||||
return result, header, nil
|
return result, header, lastOnline, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||||
@@ -313,6 +321,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
if inbound.Protocol != model.VLESS {
|
if inbound.Protocol != model.VLESS {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
var vlessSettings model.VLESSSettings
|
||||||
|
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
||||||
|
|
||||||
var stream map[string]any
|
var stream map[string]any
|
||||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||||
clients, _ := s.inboundService.GetClients(inbound)
|
clients, _ := s.inboundService.GetClients(inbound)
|
||||||
@@ -327,6 +338,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||||||
port := inbound.Port
|
port := inbound.Port
|
||||||
streamNetwork := stream["network"].(string)
|
streamNetwork := stream["network"].(string)
|
||||||
params := make(map[string]string)
|
params := make(map[string]string)
|
||||||
|
if vlessSettings.Encryption != "" {
|
||||||
|
params["encryption"] = vlessSettings.Encryption
|
||||||
|
}
|
||||||
params["type"] = streamNetwork
|
params["type"] = streamNetwork
|
||||||
|
|
||||||
switch streamNetwork {
|
switch streamNetwork {
|
||||||
@@ -995,3 +1009,172 @@ func searchHost(headers any) string {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PageData is a view model for subscription.html
|
||||||
|
type PageData struct {
|
||||||
|
Host string
|
||||||
|
BasePath string
|
||||||
|
SId string
|
||||||
|
Download string
|
||||||
|
Upload string
|
||||||
|
Total string
|
||||||
|
Used string
|
||||||
|
Remained string
|
||||||
|
Expire int64
|
||||||
|
LastOnline int64
|
||||||
|
Datepicker string
|
||||||
|
DownloadByte int64
|
||||||
|
UploadByte int64
|
||||||
|
TotalByte int64
|
||||||
|
SubUrl string
|
||||||
|
SubJsonUrl string
|
||||||
|
Result []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolveRequest extracts scheme and host info from request/headers consistently.
|
||||||
|
func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
|
||||||
|
// scheme
|
||||||
|
scheme = "http"
|
||||||
|
if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
// base host (no port)
|
||||||
|
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" {
|
||||||
|
host = h
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
host = c.GetHeader("X-Real-IP")
|
||||||
|
}
|
||||||
|
if host == "" {
|
||||||
|
var err error
|
||||||
|
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||||
|
if err != nil {
|
||||||
|
host = c.Request.Host
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// host:port for URLs
|
||||||
|
hostWithPort = c.GetHeader("X-Forwarded-Host")
|
||||||
|
if hostWithPort == "" {
|
||||||
|
hostWithPort = c.Request.Host
|
||||||
|
}
|
||||||
|
if hostWithPort == "" {
|
||||||
|
hostWithPort = host
|
||||||
|
}
|
||||||
|
|
||||||
|
// header display host
|
||||||
|
hostHeader = c.GetHeader("X-Forwarded-Host")
|
||||||
|
if hostHeader == "" {
|
||||||
|
hostHeader = c.GetHeader("X-Real-IP")
|
||||||
|
}
|
||||||
|
if hostHeader == "" {
|
||||||
|
hostHeader = host
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildURLs constructs absolute subscription and json URLs.
|
||||||
|
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
||||||
|
if strings.HasSuffix(subPath, "/") {
|
||||||
|
subURL = scheme + "://" + hostWithPort + subPath + subId
|
||||||
|
} else {
|
||||||
|
subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(subJsonPath, "/") {
|
||||||
|
subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId
|
||||||
|
} else {
|
||||||
|
subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPageData parses header and prepares the template view model.
|
||||||
|
func (s *SubService) BuildPageData(subId, hostHeader, header string, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
|
||||||
|
// Parse header values
|
||||||
|
var uploadByte, downloadByte, totalByte, expire int64
|
||||||
|
parts := strings.Split(header, ";")
|
||||||
|
for _, p := range parts {
|
||||||
|
kv := strings.Split(strings.TrimSpace(p), "=")
|
||||||
|
if len(kv) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := strings.ToLower(strings.TrimSpace(kv[0]))
|
||||||
|
val := strings.TrimSpace(kv[1])
|
||||||
|
switch key {
|
||||||
|
case "upload":
|
||||||
|
if v, err := parseInt64(val); err == nil {
|
||||||
|
uploadByte = v
|
||||||
|
}
|
||||||
|
case "download":
|
||||||
|
if v, err := parseInt64(val); err == nil {
|
||||||
|
downloadByte = v
|
||||||
|
}
|
||||||
|
case "total":
|
||||||
|
if v, err := parseInt64(val); err == nil {
|
||||||
|
totalByte = v
|
||||||
|
}
|
||||||
|
case "expire":
|
||||||
|
if v, err := parseInt64(val); err == nil {
|
||||||
|
expire = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
download := common.FormatTraffic(downloadByte)
|
||||||
|
upload := common.FormatTraffic(uploadByte)
|
||||||
|
total := "∞"
|
||||||
|
used := common.FormatTraffic(uploadByte + downloadByte)
|
||||||
|
remained := ""
|
||||||
|
if totalByte > 0 {
|
||||||
|
total = common.FormatTraffic(totalByte)
|
||||||
|
left := totalByte - (uploadByte + downloadByte)
|
||||||
|
if left < 0 {
|
||||||
|
left = 0
|
||||||
|
}
|
||||||
|
remained = common.FormatTraffic(left)
|
||||||
|
}
|
||||||
|
|
||||||
|
datepicker := s.datepicker
|
||||||
|
if datepicker == "" {
|
||||||
|
datepicker = "gregorian"
|
||||||
|
}
|
||||||
|
|
||||||
|
return PageData{
|
||||||
|
Host: hostHeader,
|
||||||
|
BasePath: "/",
|
||||||
|
SId: subId,
|
||||||
|
Download: download,
|
||||||
|
Upload: upload,
|
||||||
|
Total: total,
|
||||||
|
Used: used,
|
||||||
|
Remained: remained,
|
||||||
|
Expire: expire,
|
||||||
|
LastOnline: lastOnline,
|
||||||
|
Datepicker: datepicker,
|
||||||
|
DownloadByte: downloadByte,
|
||||||
|
UploadByte: uploadByte,
|
||||||
|
TotalByte: totalByte,
|
||||||
|
SubUrl: subURL,
|
||||||
|
SubJsonUrl: subJsonURL,
|
||||||
|
Result: subs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHostFromXFH(s string) (string, error) {
|
||||||
|
if strings.Contains(s, ":") {
|
||||||
|
realHost, _, err := net.SplitHostPort(s)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return realHost, nil
|
||||||
|
}
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseInt64(s string) (int64, error) {
|
||||||
|
// handle potential quotes
|
||||||
|
s = strings.Trim(s, "\"'")
|
||||||
|
n, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|||||||
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -49,8 +49,8 @@ class DBInbound {
|
|||||||
return this.protocol === Protocols.SHADOWSOCKS;
|
return this.protocol === Protocols.SHADOWSOCKS;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isSocks() {
|
get isMixed() {
|
||||||
return this.protocol === Protocols.SOCKS;
|
return this.protocol === Protocols.MIXED;
|
||||||
}
|
}
|
||||||
|
|
||||||
get isHTTP() {
|
get isHTTP() {
|
||||||
|
|||||||
@@ -3,18 +3,16 @@ const Protocols = {
|
|||||||
VLESS: 'vless',
|
VLESS: 'vless',
|
||||||
TROJAN: 'trojan',
|
TROJAN: 'trojan',
|
||||||
SHADOWSOCKS: 'shadowsocks',
|
SHADOWSOCKS: 'shadowsocks',
|
||||||
DOKODEMO: 'dokodemo-door',
|
TUNNEL: 'tunnel',
|
||||||
SOCKS: 'socks',
|
MIXED: 'mixed',
|
||||||
HTTP: 'http',
|
HTTP: 'http',
|
||||||
WIREGUARD: 'wireguard',
|
WIREGUARD: 'wireguard',
|
||||||
};
|
};
|
||||||
|
|
||||||
const SSMethods = {
|
const SSMethods = {
|
||||||
AES_256_GCM: 'aes-256-gcm',
|
AES_256_GCM: 'aes-256-gcm',
|
||||||
AES_128_GCM: 'aes-128-gcm',
|
|
||||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||||
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
|
||||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||||
@@ -731,7 +729,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||||||
constructor(
|
constructor(
|
||||||
show = false,
|
show = false,
|
||||||
xver = 0,
|
xver = 0,
|
||||||
dest = 'google.com:443',
|
target = 'google.com:443',
|
||||||
serverNames = 'google.com,www.google.com',
|
serverNames = 'google.com,www.google.com',
|
||||||
privateKey = '',
|
privateKey = '',
|
||||||
minClientVer = '',
|
minClientVer = '',
|
||||||
@@ -744,7 +742,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||||||
super();
|
super();
|
||||||
this.show = show;
|
this.show = show;
|
||||||
this.xver = xver;
|
this.xver = xver;
|
||||||
this.dest = dest;
|
this.target = target;
|
||||||
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
|
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
|
||||||
this.privateKey = privateKey;
|
this.privateKey = privateKey;
|
||||||
this.minClientVer = minClientVer;
|
this.minClientVer = minClientVer;
|
||||||
@@ -769,7 +767,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||||||
return new RealityStreamSettings(
|
return new RealityStreamSettings(
|
||||||
json.show,
|
json.show,
|
||||||
json.xver,
|
json.xver,
|
||||||
json.dest,
|
json.target,
|
||||||
json.serverNames,
|
json.serverNames,
|
||||||
json.privateKey,
|
json.privateKey,
|
||||||
json.minClientVer,
|
json.minClientVer,
|
||||||
@@ -785,7 +783,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||||||
return {
|
return {
|
||||||
show: this.show,
|
show: this.show,
|
||||||
xver: this.xver,
|
xver: this.xver,
|
||||||
dest: this.dest,
|
target: this.target,
|
||||||
serverNames: this.serverNames.split(","),
|
serverNames: this.serverNames.split(","),
|
||||||
privateKey: this.privateKey,
|
privateKey: this.privateKey,
|
||||||
minClientVer: this.minClientVer,
|
minClientVer: this.minClientVer,
|
||||||
@@ -1301,6 +1299,7 @@ class Inbound extends XrayCommonClass {
|
|||||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||||
const params = new Map();
|
const params = new Map();
|
||||||
params.set("type", this.stream.network);
|
params.set("type", this.stream.network);
|
||||||
|
params.set("encryption", this.settings.encryption);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "tcp":
|
case "tcp":
|
||||||
const tcp = this.stream.tcp;
|
const tcp = this.stream.tcp;
|
||||||
@@ -1713,8 +1712,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||||||
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
|
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
|
||||||
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
|
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
|
||||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
|
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
|
||||||
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
|
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
|
||||||
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
|
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
|
||||||
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
||||||
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
||||||
default: return null;
|
default: return null;
|
||||||
@@ -1727,8 +1726,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
|||||||
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
|
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
|
||||||
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
|
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
|
||||||
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
|
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
|
||||||
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
|
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
|
||||||
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
|
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
|
||||||
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
||||||
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
||||||
default: return null;
|
default: return null;
|
||||||
@@ -1859,13 +1858,17 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
constructor(
|
constructor(
|
||||||
protocol,
|
protocol,
|
||||||
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
||||||
decryption = 'none',
|
decryption = "none",
|
||||||
fallbacks = []
|
encryption = "none",
|
||||||
|
fallbacks = [],
|
||||||
|
selectedAuth = undefined,
|
||||||
) {
|
) {
|
||||||
super(protocol);
|
super(protocol);
|
||||||
this.vlesses = vlesses;
|
this.vlesses = vlesses;
|
||||||
this.decryption = decryption;
|
this.decryption = decryption;
|
||||||
|
this.encryption = encryption;
|
||||||
this.fallbacks = fallbacks;
|
this.fallbacks = fallbacks;
|
||||||
|
this.selectedAuth = selectedAuth;
|
||||||
}
|
}
|
||||||
|
|
||||||
addFallback() {
|
addFallback() {
|
||||||
@@ -1876,22 +1879,43 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||||||
this.fallbacks.splice(index, 1);
|
this.fallbacks.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// decryption should be set to static value
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
return new Inbound.VLESSSettings(
|
const obj = new Inbound.VLESSSettings(
|
||||||
Protocols.VLESS,
|
Protocols.VLESS,
|
||||||
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||||
json.decryption || 'none',
|
json.decryption,
|
||||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),);
|
json.encryption,
|
||||||
|
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||||
|
json.selectedAuth
|
||||||
|
);
|
||||||
|
return obj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
toJson() {
|
toJson() {
|
||||||
return {
|
const json = {
|
||||||
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
|
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
|
||||||
decryption: this.decryption,
|
|
||||||
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (this.decryption) {
|
||||||
|
json.decryption = this.decryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.encryption) {
|
||||||
|
json.encryption = this.encryption;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.fallbacks && this.fallbacks.length > 0) {
|
||||||
|
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
|
||||||
|
}
|
||||||
|
if (this.selectedAuth) {
|
||||||
|
json.selectedAuth = this.selectedAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||||
@@ -2303,7 +2327,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Inbound.DokodemoSettings = class extends Inbound.Settings {
|
Inbound.TunnelSettings = class extends Inbound.Settings {
|
||||||
constructor(
|
constructor(
|
||||||
protocol,
|
protocol,
|
||||||
address,
|
address,
|
||||||
@@ -2321,8 +2345,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
return new Inbound.DokodemoSettings(
|
return new Inbound.TunnelSettings(
|
||||||
Protocols.DOKODEMO,
|
Protocols.TUNNEL,
|
||||||
json.address,
|
json.address,
|
||||||
json.port,
|
json.port,
|
||||||
XrayCommonClass.toHeaders(json.portMap),
|
XrayCommonClass.toHeaders(json.portMap),
|
||||||
@@ -2342,8 +2366,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Inbound.SocksSettings = class extends Inbound.Settings {
|
Inbound.MixedSettings = class extends Inbound.Settings {
|
||||||
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||||
super(protocol);
|
super(protocol);
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
this.accounts = accounts;
|
this.accounts = accounts;
|
||||||
@@ -2363,11 +2387,11 @@ Inbound.SocksSettings = class extends Inbound.Settings {
|
|||||||
let accounts;
|
let accounts;
|
||||||
if (json.auth === 'password') {
|
if (json.auth === 'password') {
|
||||||
accounts = json.accounts.map(
|
accounts = json.accounts.map(
|
||||||
account => Inbound.SocksSettings.SocksAccount.fromJson(account)
|
account => Inbound.MixedSettings.SocksAccount.fromJson(account)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return new Inbound.SocksSettings(
|
return new Inbound.MixedSettings(
|
||||||
Protocols.SOCKS,
|
Protocols.MIXED,
|
||||||
json.auth,
|
json.auth,
|
||||||
accounts,
|
accounts,
|
||||||
json.udp,
|
json.udp,
|
||||||
@@ -2384,7 +2408,7 @@ Inbound.SocksSettings = class extends Inbound.Settings {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
|
Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
|
||||||
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
|
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
|
||||||
super();
|
super();
|
||||||
this.user = user;
|
this.user = user;
|
||||||
@@ -2392,7 +2416,7 @@ Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
|
return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -813,7 +813,7 @@ class Outbound extends CommonClass {
|
|||||||
var settings;
|
var settings;
|
||||||
switch (protocol) {
|
switch (protocol) {
|
||||||
case Protocols.VLESS:
|
case Protocols.VLESS:
|
||||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
|
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');
|
||||||
break;
|
break;
|
||||||
case Protocols.Trojan:
|
case Protocols.Trojan:
|
||||||
settings = new Outbound.TrojanSettings(address, port, userData);
|
settings = new Outbound.TrojanSettings(address, port, userData);
|
||||||
@@ -1046,13 +1046,13 @@ Outbound.VmessSettings = class extends CommonClass {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
Outbound.VLESSSettings = class extends CommonClass {
|
Outbound.VLESSSettings = class extends CommonClass {
|
||||||
constructor(address, port, id, flow, encryption = 'none') {
|
constructor(address, port, id, flow, encryption) {
|
||||||
super();
|
super();
|
||||||
this.address = address;
|
this.address = address;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.flow = flow;
|
this.flow = flow;
|
||||||
this.encryption = encryption
|
this.encryption = encryption;
|
||||||
}
|
}
|
||||||
|
|
||||||
static fromJson(json = {}) {
|
static fromJson(json = {}) {
|
||||||
@@ -1071,7 +1071,7 @@ Outbound.VLESSSettings = class extends CommonClass {
|
|||||||
vnext: [{
|
vnext: [{
|
||||||
address: this.address,
|
address: this.address,
|
||||||
port: this.port,
|
port: this.port,
|
||||||
users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
|
users: [{ id: this.id, flow: this.flow, encryption: this.encryption }],
|
||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
125
web/assets/js/subscription.js
Normal file
125
web/assets/js/subscription.js
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
(function () {
|
||||||
|
// Vue app for Subscription page
|
||||||
|
const el = document.getElementById('subscription-data');
|
||||||
|
if (!el) return;
|
||||||
|
const textarea = document.getElementById('subscription-links');
|
||||||
|
const rawLinks = (textarea?.value || '').split('\n').filter(Boolean);
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
sId: el.getAttribute('data-sid') || '',
|
||||||
|
subUrl: el.getAttribute('data-sub-url') || '',
|
||||||
|
subJsonUrl: el.getAttribute('data-subjson-url') || '',
|
||||||
|
download: el.getAttribute('data-download') || '',
|
||||||
|
upload: el.getAttribute('data-upload') || '',
|
||||||
|
used: el.getAttribute('data-used') || '',
|
||||||
|
total: el.getAttribute('data-total') || '',
|
||||||
|
remained: el.getAttribute('data-remained') || '',
|
||||||
|
expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,
|
||||||
|
lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),
|
||||||
|
downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,
|
||||||
|
uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,
|
||||||
|
totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,
|
||||||
|
datepicker: el.getAttribute('data-datepicker') || 'gregorian',
|
||||||
|
};
|
||||||
|
|
||||||
|
// Normalize lastOnline to milliseconds if it looks like seconds
|
||||||
|
if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {
|
||||||
|
data.lastOnlineMs *= 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderLink(item) {
|
||||||
|
return (
|
||||||
|
Vue.h('a-list-item', {}, [
|
||||||
|
Vue.h('a-space', { props: { size: 'small' } }, [
|
||||||
|
Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),
|
||||||
|
Vue.h('span', { class: 'break-all' }, item)
|
||||||
|
])
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function copy(text) {
|
||||||
|
ClipboardManager.copyText(text).then(ok => {
|
||||||
|
const messageType = ok ? 'success' : 'error';
|
||||||
|
Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function open(url) {
|
||||||
|
window.location.href = url;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawQR(value) {
|
||||||
|
try { new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); } catch (e) { console.warn(e); }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract a human label (email/ps) from different link types
|
||||||
|
function linkName(link, idx) {
|
||||||
|
try {
|
||||||
|
if (link.startsWith('vmess://')) {
|
||||||
|
const json = JSON.parse(atob(link.replace('vmess://', '')));
|
||||||
|
if (json.ps) return json.ps;
|
||||||
|
if (json.add && json.id) return json.add; // fallback host
|
||||||
|
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
|
||||||
|
// vless://<id>@host:port?...#name
|
||||||
|
const hashIdx = link.indexOf('#');
|
||||||
|
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||||
|
// email sometimes in query params like sni or remark
|
||||||
|
const qIdx = link.indexOf('?');
|
||||||
|
if (qIdx !== -1) {
|
||||||
|
const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
|
||||||
|
if (qs.get('remark')) return qs.get('remark');
|
||||||
|
if (qs.get('email')) return qs.get('email');
|
||||||
|
}
|
||||||
|
// else take user@host
|
||||||
|
const at = link.indexOf('@');
|
||||||
|
const protSep = link.indexOf('://');
|
||||||
|
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
|
||||||
|
} else if (link.startsWith('ss://')) {
|
||||||
|
// shadowsocks: label often after #
|
||||||
|
const hashIdx = link.indexOf('#');
|
||||||
|
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore and fallback */ }
|
||||||
|
return 'Link ' + (idx + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Vue({
|
||||||
|
delimiters: ['[[', ']]'],
|
||||||
|
el: '#app',
|
||||||
|
data: {
|
||||||
|
themeSwitcher,
|
||||||
|
app: data,
|
||||||
|
links: rawLinks,
|
||||||
|
lang: '',
|
||||||
|
viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
this.lang = LanguageManager.getLanguage();
|
||||||
|
// Discover subJsonUrl if provided via template bootstrap
|
||||||
|
const tpl = document.getElementById('subscription-data');
|
||||||
|
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
|
||||||
|
if (sj) this.app.subJsonUrl = sj;
|
||||||
|
drawQR(this.app.subUrl);
|
||||||
|
// Draw second QR if available
|
||||||
|
try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ }
|
||||||
|
// Track viewport width for responsive behavior
|
||||||
|
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
||||||
|
window.addEventListener('resize', this._onResize);
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isMobile() { return this.viewportWidth < 576; },
|
||||||
|
isUnlimited() { return !this.app.totalByte; },
|
||||||
|
isActive() {
|
||||||
|
const now = Date.now();
|
||||||
|
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
|
||||||
|
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
|
||||||
|
return expiryOk && trafficOk;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: { renderLink, copy, open, linkName, i18nLabel(key) { return '{{ i18n "' + key + '" }}'; } },
|
||||||
|
});
|
||||||
|
})();
|
||||||
@@ -134,7 +134,7 @@ class DateUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static formatMillis(millis) {
|
static formatMillis(millis) {
|
||||||
return moment(millis).format('YYYY-M-D H:m:s');
|
return moment(millis).format('YYYY-M-D HH:mm:ss');
|
||||||
}
|
}
|
||||||
|
|
||||||
static firstDayOfMonth() {
|
static firstDayOfMonth() {
|
||||||
|
|||||||
@@ -326,6 +326,14 @@ class ObjectUtil {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (const key in b) {
|
||||||
|
if (!b.hasOwnProperty(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!a.hasOwnProperty(key)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
type APIController struct {
|
type APIController struct {
|
||||||
BaseController
|
BaseController
|
||||||
inboundController *InboundController
|
inboundController *InboundController
|
||||||
|
serverController *ServerController
|
||||||
Tgbot service.Tgbot
|
Tgbot service.Tgbot
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -19,42 +20,22 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/panel/api/inbounds")
|
// Main API group
|
||||||
g.Use(a.checkLogin)
|
api := g.Group("/panel/api")
|
||||||
|
api.Use(a.checkLogin)
|
||||||
|
|
||||||
a.inboundController = NewInboundController(g)
|
// Inbounds API
|
||||||
|
inbounds := api.Group("/inbounds")
|
||||||
|
a.inboundController = NewInboundController(inbounds)
|
||||||
|
|
||||||
inboundRoutes := []struct {
|
// Server API
|
||||||
Method string
|
server := api.Group("/server")
|
||||||
Path string
|
a.serverController = NewServerController(server)
|
||||||
Handler gin.HandlerFunc
|
|
||||||
}{
|
|
||||||
{"GET", "/createbackup", a.createBackup},
|
|
||||||
{"GET", "/list", a.inboundController.getInbounds},
|
|
||||||
{"GET", "/get/:id", a.inboundController.getInbound},
|
|
||||||
{"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
|
|
||||||
{"GET", "/getClientTrafficsById/:id", a.inboundController.getClientTrafficsById},
|
|
||||||
{"POST", "/add", a.inboundController.addInbound},
|
|
||||||
{"POST", "/del/:id", a.inboundController.delInbound},
|
|
||||||
{"POST", "/update/:id", a.inboundController.updateInbound},
|
|
||||||
{"POST", "/clientIps/:email", a.inboundController.getClientIps},
|
|
||||||
{"POST", "/clearClientIps/:email", a.inboundController.clearClientIps},
|
|
||||||
{"POST", "/addClient", a.inboundController.addInboundClient},
|
|
||||||
{"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient},
|
|
||||||
{"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient},
|
|
||||||
{"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic},
|
|
||||||
{"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics},
|
|
||||||
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
|
||||||
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
|
||||||
{"POST", "/onlines", a.inboundController.onlines},
|
|
||||||
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, route := range inboundRoutes {
|
// Extra routes
|
||||||
g.Handle(route.Method, route.Path, route.Handler)
|
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *APIController) createBackup(c *gin.Context) {
|
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
||||||
a.Tgbot.SendBackupToAdmins()
|
a.Tgbot.SendBackupToAdmins()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,9 +24,12 @@ func NewInboundController(g *gin.RouterGroup) *InboundController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/inbound")
|
|
||||||
|
|
||||||
g.POST("/list", a.getInbounds)
|
g.GET("/list", a.getInbounds)
|
||||||
|
g.GET("/get/:id", a.getInbound)
|
||||||
|
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||||
|
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
|
||||||
|
|
||||||
g.POST("/add", a.addInbound)
|
g.POST("/add", a.addInbound)
|
||||||
g.POST("/del/:id", a.delInbound)
|
g.POST("/del/:id", a.delInbound)
|
||||||
g.POST("/update/:id", a.updateInbound)
|
g.POST("/update/:id", a.updateInbound)
|
||||||
@@ -41,6 +44,9 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||||
g.POST("/import", a.importInbound)
|
g.POST("/import", a.importInbound)
|
||||||
g.POST("/onlines", a.onlines)
|
g.POST("/onlines", a.onlines)
|
||||||
|
g.POST("/lastOnline", a.lastOnline)
|
||||||
|
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
||||||
|
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||||
@@ -340,6 +346,11 @@ func (a *InboundController) onlines(c *gin.Context) {
|
|||||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||||
|
data, err := a.inboundService.GetClientsLastOnline()
|
||||||
|
jsonObj(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||||
email := c.Param("email")
|
email := c.Param("email")
|
||||||
|
|
||||||
@@ -364,3 +375,23 @@ func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
|||||||
|
|
||||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
||||||
|
inboundId, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Invalid inbound ID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
email := c.Param("email")
|
||||||
|
needRestart, err := a.inboundService.DelInboundClientByEmail(inboundId, email)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Failed to delete client by email", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonMsg(c, "Client deleted successfully", nil)
|
||||||
|
if needRestart {
|
||||||
|
a.xrayService.SetToNeedRestart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,11 +37,17 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||||
g = g.Group("/server")
|
|
||||||
|
|
||||||
g.Use(a.checkLogin)
|
g.GET("/status", a.status)
|
||||||
g.POST("/status", a.status)
|
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||||
g.POST("/getXrayVersion", a.getXrayVersion)
|
g.GET("/getConfigJson", a.getConfigJson)
|
||||||
|
g.GET("/getDb", a.getDb)
|
||||||
|
g.GET("/getNewUUID", a.getNewUUID)
|
||||||
|
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
||||||
|
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
||||||
|
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
||||||
|
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
|
||||||
|
|
||||||
g.POST("/stopXrayService", a.stopXrayService)
|
g.POST("/stopXrayService", a.stopXrayService)
|
||||||
g.POST("/restartXrayService", a.restartXrayService)
|
g.POST("/restartXrayService", a.restartXrayService)
|
||||||
g.POST("/installXray/:version", a.installXray)
|
g.POST("/installXray/:version", a.installXray)
|
||||||
@@ -49,11 +55,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.POST("/updateGeofile/:fileName", a.updateGeofile)
|
g.POST("/updateGeofile/:fileName", a.updateGeofile)
|
||||||
g.POST("/logs/:count", a.getLogs)
|
g.POST("/logs/:count", a.getLogs)
|
||||||
g.POST("/xraylogs/:count", a.getXrayLogs)
|
g.POST("/xraylogs/:count", a.getXrayLogs)
|
||||||
g.POST("/getConfigJson", a.getConfigJson)
|
|
||||||
g.GET("/getDb", a.getDb)
|
|
||||||
g.POST("/importDB", a.importDB)
|
g.POST("/importDB", a.importDB)
|
||||||
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
|
||||||
g.POST("/getNewmldsa65", a.getNewmldsa65)
|
|
||||||
g.POST("/getNewEchCert", a.getNewEchCert)
|
g.POST("/getNewEchCert", a.getNewEchCert)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,3 +268,31 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
jsonObj(c, cert, nil)
|
jsonObj(c, cert, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||||
|
out, err := a.serverService.GetNewVlessEnc()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, out, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getNewUUID(c *gin.Context) {
|
||||||
|
uuidResp, err := a.serverService.GetNewUUID()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Failed to generate UUID", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonObj(c, uuidResp, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
||||||
|
out, err := a.serverService.GetNewmlkem768()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "Failed to generate mlkem768 keys", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, out, nil)
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type XUIController struct {
|
|||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
inboundController *InboundController
|
inboundController *InboundController
|
||||||
|
serverController *ServerController
|
||||||
settingController *SettingController
|
settingController *SettingController
|
||||||
xraySettingController *XraySettingController
|
xraySettingController *XraySettingController
|
||||||
}
|
}
|
||||||
@@ -28,6 +29,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
|||||||
g.GET("/xray", a.xraySettings)
|
g.GET("/xray", a.xraySettings)
|
||||||
|
|
||||||
a.inboundController = NewInboundController(g)
|
a.inboundController = NewInboundController(g)
|
||||||
|
a.serverController = NewServerController(g)
|
||||||
a.settingController = NewSettingController(g)
|
a.settingController = NewSettingController(g)
|
||||||
a.xraySettingController = NewXraySettingController(g)
|
a.xraySettingController = NewXraySettingController(g)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,12 +33,17 @@
|
|||||||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
<template slot="online" slot-scope="text, client, index">
|
<template slot="online" slot-scope="text, client, index">
|
||||||
<template v-if="client.enable && isClientOnline(client.email)">
|
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
<template slot="content" >
|
||||||
</template>
|
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||||
<template v-else>
|
</template>
|
||||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
<template v-if="client.enable && isClientOnline(client.email)">
|
||||||
</template>
|
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||||
|
</template>
|
||||||
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
<template slot="client" slot-scope="text, client">
|
<template slot="client" slot-scope="text, client">
|
||||||
<a-space direction="horizontal" :size="2">
|
<a-space direction="horizontal" :size="2">
|
||||||
|
|||||||
@@ -83,14 +83,14 @@
|
|||||||
{{template "form/shadowsocks"}}
|
{{template "form/shadowsocks"}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- dokodemo-door -->
|
<!-- tunnel -->
|
||||||
<template v-if="inbound.protocol === Protocols.DOKODEMO">
|
<template v-if="inbound.protocol === Protocols.TUNNEL">
|
||||||
{{template "form/dokodemo"}}
|
{{template "form/tunnel"}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- socks -->
|
<!-- mixed -->
|
||||||
<template v-if="inbound.protocol === Protocols.SOCKS">
|
<template v-if="inbound.protocol === Protocols.MIXED">
|
||||||
{{template "form/socks"}}
|
{{template "form/mixed"}}
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- http -->
|
<!-- http -->
|
||||||
|
|||||||
@@ -226,6 +226,11 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- vless settings -->
|
<!-- vless settings -->
|
||||||
|
<template v-if="outbound.protocol === Protocols.VLESS">
|
||||||
|
<a-form-item label='encryption'>
|
||||||
|
<a-input v-model.trim="outbound.settings.encryption"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
</template>
|
||||||
<template v-if="outbound.canEnableTlsFlow()">
|
<template v-if="outbound.canEnableTlsFlow()">
|
||||||
<a-form-item label='Flow'>
|
<a-form-item label='Flow'>
|
||||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
@@ -436,6 +441,9 @@
|
|||||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
<a-form-item label="ECH Config List">
|
||||||
|
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
|
||||||
|
</a-form-item>
|
||||||
<a-form-item label="Allow Insecure">
|
<a-form-item label="Allow Insecure">
|
||||||
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{define "form/dokodemo"}}
|
{{define "form/tunnel"}}
|
||||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
<a-form-item label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
{{define "form/socks"}}
|
{{define "form/mixed"}}
|
||||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<td width="45%">{{ i18n "username" }}</td>
|
<td width="45%">{{ i18n "username" }}</td>
|
||||||
<td width="45%">{{ i18n "password" }}</td>
|
<td width="45%">{{ i18n "password" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button>
|
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -18,7 +18,29 @@
|
|||||||
</table>
|
</table>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
<template v-if="inbound.isTcp">
|
<template v-if="!inbound.stream.isTLS || !inbound.stream.isReality">
|
||||||
|
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
|
<a-form-item label="Authentication">
|
||||||
|
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
|
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
|
||||||
|
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="decryption">
|
||||||
|
<a-input v-model.trim="inbound.settings.decryption"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label="encryption">
|
||||||
|
<a-input v-model="inbound.settings.encryption"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item label=" ">
|
||||||
|
<a-space>
|
||||||
|
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button>
|
||||||
|
<a-button danger @click="clearVlessEnc">Clear</a-button>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</template>
|
||||||
|
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
|
||||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
<a-form-item label="Fallbacks">
|
<a-form-item label="Fallbacks">
|
||||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||||
|
|||||||
@@ -12,8 +12,8 @@
|
|||||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='Dest (Target)'>
|
<a-form-item label='Target'>
|
||||||
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
|
<a-input v-model.trim="inbound.stream.reality.target"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='SNI'>
|
<a-form-item label='SNI'>
|
||||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||||
@@ -48,7 +48,10 @@
|
|||||||
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
|
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label=" ">
|
<a-form-item label=" ">
|
||||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
<a-space>
|
||||||
|
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
||||||
|
<a-button danger @click="clearX25519Cert">Clear</a-button>
|
||||||
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label="mldsa65 Seed">
|
<a-form-item label="mldsa65 Seed">
|
||||||
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
|
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
|
||||||
@@ -57,7 +60,10 @@
|
|||||||
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
|
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label=" ">
|
<a-form-item label=" ">
|
||||||
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
|
<a-space>
|
||||||
|
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
|
||||||
|
<a-button danger @click="clearMldsa65">Clear</a-button>
|
||||||
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
{{end}}
|
{{end}}
|
||||||
@@ -116,7 +116,10 @@
|
|||||||
</a-select>
|
</a-select>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label=" ">
|
<a-form-item label=" ">
|
||||||
|
<a-space>
|
||||||
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
|
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
|
||||||
|
<a-button danger @click="clearEchCert">Clear</a-button>
|
||||||
|
</a-space>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +1,8 @@
|
|||||||
{{ template "page/head_start" .}}
|
{{ template "page/head_start" .}}
|
||||||
<style>
|
|
||||||
.ant-table:not(.ant-table-expanded-row .ant-table) {
|
|
||||||
outline: 1px solid #f0f0f0;
|
|
||||||
outline-offset: -1px;
|
|
||||||
border-radius: 1rem;
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
.dark .ant-table:not(.ant-table-expanded-row .ant-table) {
|
|
||||||
outline-color: var(--dark-color-table-ring);
|
|
||||||
}
|
|
||||||
.ant-table .ant-table-content .ant-table-scroll .ant-table-body {
|
|
||||||
overflow-y: hidden;
|
|
||||||
}
|
|
||||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
|
|
||||||
margin:-10px 22px !important;
|
|
||||||
}
|
|
||||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
|
|
||||||
border-bottom-left-radius: 1rem;
|
|
||||||
border-bottom-right-radius: 1rem;
|
|
||||||
}
|
|
||||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td {
|
|
||||||
border-bottom-color: transparent;
|
|
||||||
}
|
|
||||||
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child {
|
|
||||||
border-bottom-left-radius: 6px;
|
|
||||||
}
|
|
||||||
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child {
|
|
||||||
border-bottom-right-radius: 6px;
|
|
||||||
}
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.ant-layout-content {
|
|
||||||
margin: 24px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.ant-card-body {
|
|
||||||
padding: .5rem;
|
|
||||||
}
|
|
||||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
|
|
||||||
margin:-10px 2px !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.dark .ant-switch-small:not(.ant-switch-checked) {
|
|
||||||
background-color: var(--dark-color-surface-100) !important;
|
|
||||||
}
|
|
||||||
.ant-custom-popover-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 10px;
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
.ant-col-sm-24 {
|
|
||||||
margin: 0.5rem -2rem 0.5rem 2rem;
|
|
||||||
}
|
|
||||||
tr.hideExpandIcon .ant-table-row-expand-icon {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.infinite-tag {
|
|
||||||
padding: 0 5px;
|
|
||||||
border-radius: 2rem;
|
|
||||||
min-width: 50px;
|
|
||||||
min-height: 22px;
|
|
||||||
}
|
|
||||||
.infinite-bar .ant-progress-inner .ant-progress-bg {
|
|
||||||
background-color: #F2EAF1;
|
|
||||||
border: #D5BED2 solid 1px;
|
|
||||||
}
|
|
||||||
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
|
|
||||||
background-color: #7a316f !important;
|
|
||||||
border: #7a316f solid 1px;
|
|
||||||
}
|
|
||||||
.ant-collapse {
|
|
||||||
margin: 5px 0;
|
|
||||||
}
|
|
||||||
.info-large-tag {
|
|
||||||
max-width: 200px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.client-comment {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.75;
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
.client-email {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.client-popup-item {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 5px;
|
|
||||||
}
|
|
||||||
.online-animation .ant-badge-status-dot {
|
|
||||||
animation: onlineAnimation 1.2s linear infinite;
|
|
||||||
}
|
|
||||||
@keyframes onlineAnimation {
|
|
||||||
0%,
|
|
||||||
50%,
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
transform: scale(1.5);
|
|
||||||
opacity: .2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.tr-table-box {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.tr-table-rt {
|
|
||||||
flex-basis: 70px;
|
|
||||||
min-width: 70px;
|
|
||||||
text-align: end;
|
|
||||||
}
|
|
||||||
.tr-table-lt {
|
|
||||||
flex-basis: 70px;
|
|
||||||
min-width: 70px;
|
|
||||||
text-align: start;
|
|
||||||
}
|
|
||||||
.tr-table-bar {
|
|
||||||
flex-basis: 160px;
|
|
||||||
min-width: 60px;
|
|
||||||
}
|
|
||||||
.tr-infinity-ch {
|
|
||||||
font-size: 14pt;
|
|
||||||
max-height: 24px;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.ant-table-expanded-row .ant-table .ant-table-body {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
.ant-table-expanded-row .ant-table-tbody>tr>td {
|
|
||||||
padding: 10px 2px;
|
|
||||||
}
|
|
||||||
.ant-table-expanded-row .ant-table-thead>tr>th {
|
|
||||||
padding: 12px 2px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' inbounds-page'">
|
||||||
<a-sidebar></a-sidebar>
|
<a-sidebar></a-sidebar>
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
@@ -706,7 +564,7 @@
|
|||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.enable" }}',
|
title: '{{ i18n "pages.inbounds.enable" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 30,
|
width: 35,
|
||||||
scopedSlots: { customRender: 'enable' },
|
scopedSlots: { customRender: 'enable' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||||
@@ -731,12 +589,12 @@
|
|||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.traffic" }}',
|
title: '{{ i18n "pages.inbounds.traffic" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 60,
|
width: 90,
|
||||||
scopedSlots: { customRender: 'traffic' },
|
scopedSlots: { customRender: 'traffic' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
|
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
|
||||||
align: 'center',
|
align: 'center',
|
||||||
width: 60,
|
width: 70,
|
||||||
scopedSlots: { customRender: 'allTimeInbound' },
|
scopedSlots: { customRender: 'allTimeInbound' },
|
||||||
}, {
|
}, {
|
||||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||||
@@ -770,14 +628,12 @@
|
|||||||
|
|
||||||
const innerColumns = [
|
const innerColumns = [
|
||||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
|
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
|
||||||
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
|
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||||
{ title: '{{ i18n "pages.inbounds.createdAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'createdAt' } },
|
|
||||||
{ title: '{{ i18n "pages.inbounds.updatedAt" }}', width: 90, align: 'center', scopedSlots: { customRender: 'updatedAt' } },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const innerMobileColumns = [
|
const innerMobileColumns = [
|
||||||
@@ -809,6 +665,7 @@
|
|||||||
defaultKey: '',
|
defaultKey: '',
|
||||||
clientCount: [],
|
clientCount: [],
|
||||||
onlineClients: [],
|
onlineClients: [],
|
||||||
|
lastOnlineMap: {},
|
||||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||||
@@ -831,12 +688,13 @@
|
|||||||
},
|
},
|
||||||
async getDBInbounds() {
|
async getDBInbounds() {
|
||||||
this.refreshing = true;
|
this.refreshing = true;
|
||||||
const msg = await HttpUtil.post('/panel/inbound/list');
|
const msg = await HttpUtil.get('/panel/api/inbounds/list');
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
this.refreshing = false;
|
this.refreshing = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.getLastOnlineMap();
|
||||||
await this.getOnlineUsers();
|
await this.getOnlineUsers();
|
||||||
|
|
||||||
this.setInbounds(msg.obj);
|
this.setInbounds(msg.obj);
|
||||||
@@ -845,12 +703,17 @@
|
|||||||
}, 500);
|
}, 500);
|
||||||
},
|
},
|
||||||
async getOnlineUsers() {
|
async getOnlineUsers() {
|
||||||
const msg = await HttpUtil.post('/panel/inbound/onlines');
|
const msg = await HttpUtil.post('/panel/api/inbounds/onlines');
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.onlineClients = msg.obj != null ? msg.obj : [];
|
this.onlineClients = msg.obj != null ? msg.obj : [];
|
||||||
},
|
},
|
||||||
|
async getLastOnlineMap() {
|
||||||
|
const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
|
||||||
|
if (!msg.success || !msg.obj) return;
|
||||||
|
this.lastOnlineMap = msg.obj || {}
|
||||||
|
},
|
||||||
async getDefaultSettings() {
|
async getDefaultSettings() {
|
||||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
@@ -978,11 +841,13 @@
|
|||||||
const list = this.clientCount[inbound.id][this.filterBy];
|
const list = this.clientCount[inbound.id][this.filterBy];
|
||||||
if (list.length > 0) {
|
if (list.length > 0) {
|
||||||
const filteredSettings = { "clients": [] };
|
const filteredSettings = { "clients": [] };
|
||||||
inboundSettings.clients.forEach(client => {
|
if (inboundSettings.clients) {
|
||||||
if (list.includes(client.email)) {
|
inboundSettings.clients.forEach(client => {
|
||||||
filteredSettings.clients.push(client);
|
if (list.includes(client.email)) {
|
||||||
}
|
filteredSettings.clients.push(client);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
|
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
|
||||||
this.searchedInbounds.push(newInbound);
|
this.searchedInbounds.push(newInbound);
|
||||||
}
|
}
|
||||||
@@ -1094,7 +959,7 @@
|
|||||||
streamSettings: baseInbound.stream.toString(),
|
streamSettings: baseInbound.stream.toString(),
|
||||||
sniffing: baseInbound.sniffing.toString(),
|
sniffing: baseInbound.sniffing.toString(),
|
||||||
};
|
};
|
||||||
await this.submit('/panel/inbound/add', data, inModal);
|
await this.submit('/panel/api/inbounds/add', data, inModal);
|
||||||
},
|
},
|
||||||
openAddInbound() {
|
openAddInbound() {
|
||||||
inModal.show({
|
inModal.show({
|
||||||
@@ -1143,7 +1008,7 @@
|
|||||||
}
|
}
|
||||||
data.sniffing = inbound.sniffing.toString();
|
data.sniffing = inbound.sniffing.toString();
|
||||||
|
|
||||||
await this.submit('/panel/inbound/add', data, inModal);
|
await this.submit('/panel/api/inbounds/add', data, inModal);
|
||||||
},
|
},
|
||||||
async updateInbound(inbound, dbInbound) {
|
async updateInbound(inbound, dbInbound) {
|
||||||
const data = {
|
const data = {
|
||||||
@@ -1166,7 +1031,7 @@
|
|||||||
}
|
}
|
||||||
data.sniffing = inbound.sniffing.toString();
|
data.sniffing = inbound.sniffing.toString();
|
||||||
|
|
||||||
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
|
await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);
|
||||||
},
|
},
|
||||||
openAddClient(dbInboundId) {
|
openAddClient(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
@@ -1221,14 +1086,14 @@
|
|||||||
id: dbInboundId,
|
id: dbInboundId,
|
||||||
settings: '{"clients": [' + clients.toString() + ']}',
|
settings: '{"clients": [' + clients.toString() + ']}',
|
||||||
};
|
};
|
||||||
await this.submit(`/panel/inbound/addClient`, data, modal);
|
await this.submit(`/panel/api/inbounds/addClient`, data, modal);
|
||||||
},
|
},
|
||||||
async updateClient(client, dbInboundId, clientId) {
|
async updateClient(client, dbInboundId, clientId) {
|
||||||
const data = {
|
const data = {
|
||||||
id: dbInboundId,
|
id: dbInboundId,
|
||||||
settings: '{"clients": [' + client.toString() + ']}',
|
settings: '{"clients": [' + client.toString() + ']}',
|
||||||
};
|
};
|
||||||
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
|
await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
|
||||||
},
|
},
|
||||||
resetTraffic(dbInboundId) {
|
resetTraffic(dbInboundId) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
@@ -1253,7 +1118,7 @@
|
|||||||
class: themeSwitcher.currentTheme,
|
class: themeSwitcher.currentTheme,
|
||||||
okText: '{{ i18n "delete"}}',
|
okText: '{{ i18n "delete"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
|
onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
delClient(dbInboundId, client,confirmation = true) {
|
delClient(dbInboundId, client,confirmation = true) {
|
||||||
@@ -1266,10 +1131,10 @@
|
|||||||
class: themeSwitcher.currentTheme,
|
class: themeSwitcher.currentTheme,
|
||||||
okText: '{{ i18n "delete"}}',
|
okText: '{{ i18n "delete"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
|
onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
|
this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
getSubGroupClients(dbInbounds, currentClient) {
|
getSubGroupClients(dbInbounds, currentClient) {
|
||||||
@@ -1348,7 +1213,7 @@
|
|||||||
switchEnable(dbInboundId,state) {
|
switchEnable(dbInboundId,state) {
|
||||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||||
dbInbound.enable = state;
|
dbInbound.enable = state;
|
||||||
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
|
this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);
|
||||||
},
|
},
|
||||||
async switchEnableClient(dbInboundId, client) {
|
async switchEnableClient(dbInboundId, client) {
|
||||||
this.loading()
|
this.loading()
|
||||||
@@ -1378,10 +1243,10 @@
|
|||||||
class: themeSwitcher.currentTheme,
|
class: themeSwitcher.currentTheme,
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
|
onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
|
this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetAllTraffic() {
|
resetAllTraffic() {
|
||||||
@@ -1391,7 +1256,7 @@
|
|||||||
class: themeSwitcher.currentTheme,
|
class: themeSwitcher.currentTheme,
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
|
onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
resetAllClientTraffics(dbInboundId) {
|
resetAllClientTraffics(dbInboundId) {
|
||||||
@@ -1401,7 +1266,7 @@
|
|||||||
class: themeSwitcher.currentTheme,
|
class: themeSwitcher.currentTheme,
|
||||||
okText: '{{ i18n "reset"}}',
|
okText: '{{ i18n "reset"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
|
onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
delDepletedClients(dbInboundId) {
|
delDepletedClients(dbInboundId) {
|
||||||
@@ -1411,7 +1276,7 @@
|
|||||||
class: themeSwitcher.currentTheme,
|
class: themeSwitcher.currentTheme,
|
||||||
okText: '{{ i18n "delete"}}',
|
okText: '{{ i18n "delete"}}',
|
||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId),
|
onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
isExpiry(dbInbound, index) {
|
isExpiry(dbInbound, index) {
|
||||||
@@ -1495,6 +1360,17 @@
|
|||||||
isClientOnline(email) {
|
isClientOnline(email) {
|
||||||
return this.onlineClients.includes(email);
|
return this.onlineClients.includes(email);
|
||||||
},
|
},
|
||||||
|
getLastOnline(email) {
|
||||||
|
return this.lastOnlineMap[email] || null
|
||||||
|
},
|
||||||
|
formatLastOnline(email) {
|
||||||
|
const ts = this.getLastOnline(email)
|
||||||
|
if (!ts) return '-'
|
||||||
|
if (this.datepicker === 'gregorian') {
|
||||||
|
return DateUtil.formatMillis(ts)
|
||||||
|
}
|
||||||
|
return DateUtil.convertToJalalian(moment(ts))
|
||||||
|
},
|
||||||
isRemovable(dbInboundId) {
|
isRemovable(dbInboundId) {
|
||||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||||
},
|
},
|
||||||
@@ -1526,7 +1402,7 @@
|
|||||||
value: '',
|
value: '',
|
||||||
okText: '{{ i18n "pages.inbounds.import" }}',
|
okText: '{{ i18n "pages.inbounds.import" }}',
|
||||||
confirm: async (dbInboundText) => {
|
confirm: async (dbInboundText) => {
|
||||||
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal);
|
await this.submit('/panel/api/inbounds/import', {data: dbInboundText}, promptModal);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,73 +1,4 @@
|
|||||||
{{ template "page/head_start" .}}
|
{{ template "page/head_start" .}}
|
||||||
<style>
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.ant-layout-content {
|
|
||||||
margin: 24px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-card-dark h2 {
|
|
||||||
color: var(--dark-color-text-primary);
|
|
||||||
}
|
|
||||||
.ant-backup-list-item {
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.ant-version-list-item {
|
|
||||||
--padding: 12px;
|
|
||||||
padding: var(--padding) !important;
|
|
||||||
gap: var(--padding);
|
|
||||||
}
|
|
||||||
.dark .ant-version-list-item svg{
|
|
||||||
color: var(--dark-color-text-primary);
|
|
||||||
}
|
|
||||||
.dark .ant-backup-list-item svg,
|
|
||||||
.dark .ant-badge-status-text,
|
|
||||||
.dark .ant-card-extra {
|
|
||||||
color: var(--dark-color-text-primary);
|
|
||||||
}
|
|
||||||
.dark .ant-card-actions>li {
|
|
||||||
color: rgba(255, 255, 255, 0.55);
|
|
||||||
}
|
|
||||||
.dark .ant-radio-inner {
|
|
||||||
background-color: var(--dark-color-surface-100);
|
|
||||||
border-color: var(--dark-color-surface-600);
|
|
||||||
}
|
|
||||||
.dark .ant-radio-checked .ant-radio-inner {
|
|
||||||
border-color: var(--color-primary-100);
|
|
||||||
}
|
|
||||||
.dark .ant-backup-list,
|
|
||||||
.dark .ant-version-list,
|
|
||||||
.dark .ant-card-actions,
|
|
||||||
.dark .ant-card-actions>li:not(:last-child) {
|
|
||||||
border-color: var(--dark-color-stroke);
|
|
||||||
}
|
|
||||||
.ant-card-actions {
|
|
||||||
background: transparent;
|
|
||||||
}
|
|
||||||
.ip-hidden {
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
user-select: none;
|
|
||||||
filter: blur(10px);
|
|
||||||
}
|
|
||||||
.running-animation .ant-badge-status-dot {
|
|
||||||
animation: runningAnimation 1.2s linear infinite;
|
|
||||||
}
|
|
||||||
.running-animation .ant-badge-status-processing:after {
|
|
||||||
border-color: var(--color-primary-100);
|
|
||||||
}
|
|
||||||
@keyframes runningAnimation {
|
|
||||||
0%,
|
|
||||||
50%,
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
10% {
|
|
||||||
transform: scale(1.5);
|
|
||||||
opacity: .2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
@@ -77,7 +8,7 @@
|
|||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
|
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
<a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
|
||||||
message='{{ i18n "secAlertTitle" }}'
|
message='{{ i18n "secAlertTitle" }}'
|
||||||
color="red"
|
color="red"
|
||||||
description='{{ i18n "secAlertSsl" }}'
|
description='{{ i18n "secAlertSsl" }}'
|
||||||
@@ -87,7 +18,7 @@
|
|||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<template>
|
<template>
|
||||||
<a-row v-if="!loadingStates.fetched">
|
<a-row v-if="!loadingStates.fetched">
|
||||||
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
<a-card class="card-placeholder text-center">
|
||||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
||||||
</a-card>
|
</a-card>
|
||||||
</a-row>
|
</a-row>
|
||||||
@@ -97,7 +28,7 @@
|
|||||||
<a-row :gutter="[0, isMobile ? 16 : 0]">
|
<a-row :gutter="[0, isMobile ? 16 : 0]">
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.cpu.color"
|
:stroke-color="status.cpu.color"
|
||||||
:percent="status.cpu.percent"></a-progress>
|
:percent="status.cpu.percent"></a-progress>
|
||||||
@@ -112,7 +43,7 @@
|
|||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.mem.color"
|
:stroke-color="status.mem.color"
|
||||||
:percent="status.mem.percent"></a-progress>
|
:percent="status.mem.percent"></a-progress>
|
||||||
@@ -124,7 +55,7 @@
|
|||||||
</a-col>
|
</a-col>
|
||||||
<a-col :sm="24" :md="12">
|
<a-col :sm="24" :md="12">
|
||||||
<a-row>
|
<a-row>
|
||||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.swap.color"
|
:stroke-color="status.swap.color"
|
||||||
:percent="status.swap.percent"></a-progress>
|
:percent="status.swap.percent"></a-progress>
|
||||||
@@ -132,7 +63,7 @@
|
|||||||
<b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
|
<b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
|
||||||
</div>
|
</div>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
<a-col :span="12" class="text-center">
|
||||||
<a-progress type="dashboard" status="normal"
|
<a-progress type="dashboard" status="normal"
|
||||||
:stroke-color="status.disk.color"
|
:stroke-color="status.disk.color"
|
||||||
:percent="status.disk.percent"></a-progress>
|
:percent="status.disk.percent"></a-progress>
|
||||||
@@ -167,31 +98,31 @@
|
|||||||
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
|
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col>
|
<a-col>
|
||||||
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
|
<a-icon type="bars" class="cursor-pointer float-right" @click="openLogs()"></a-icon>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</span>
|
</span>
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
|
<span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
|
||||||
</template>
|
</template>
|
||||||
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
|
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" :style="{ justifyContent: 'center' }">
|
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" class="jc-center">
|
||||||
<a-icon type="bars"></a-icon>
|
<a-icon type="bars"></a-icon>
|
||||||
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
|
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
|
<a-space direction="horizontal" @click="stopXrayService" class="jc-center">
|
||||||
<a-icon type="poweroff"></a-icon>
|
<a-icon type="poweroff"></a-icon>
|
||||||
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
|
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }">
|
<a-space direction="horizontal" @click="restartXrayService" class="jc-center">
|
||||||
<a-icon type="reload"></a-icon>
|
<a-icon type="reload"></a-icon>
|
||||||
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
|
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }">
|
<a-space direction="horizontal" @click="openSelectV2rayVersion" class="jc-center">
|
||||||
<a-icon type="tool"></a-icon>
|
<a-icon type="tool"></a-icon>
|
||||||
<span v-if="!isMobile">
|
<span v-if="!isMobile">
|
||||||
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
|
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
|
||||||
@@ -203,15 +134,15 @@
|
|||||||
<a-col :sm="24" :lg="12">
|
<a-col :sm="24" :lg="12">
|
||||||
<a-card title='{{ i18n "menu.link" }}' hoverable>
|
<a-card title='{{ i18n "menu.link" }}' hoverable>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<a-space direction="horizontal" @click="openLogs()" :style="{ justifyContent: 'center' }">
|
<a-space direction="horizontal" @click="openLogs()" class="jc-center">
|
||||||
<a-icon type="bars"></a-icon>
|
<a-icon type="bars"></a-icon>
|
||||||
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
|
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" @click="openConfig" :style="{ justifyContent: 'center' }">
|
<a-space direction="horizontal" @click="openConfig" class="jc-center">
|
||||||
<a-icon type="control"></a-icon>
|
<a-icon type="control"></a-icon>
|
||||||
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
|
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
<a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }">
|
<a-space direction="horizontal" @click="openBackup" class="jc-center">
|
||||||
<a-icon type="cloud-server"></a-icon>
|
<a-icon type="cloud-server"></a-icon>
|
||||||
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
|
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
|
||||||
</a-space>
|
</a-space>
|
||||||
@@ -314,7 +245,7 @@
|
|||||||
<template #title>
|
<template #title>
|
||||||
{{ i18n "pages.index.toggleIpVisibility" }}
|
{{ i18n "pages.index.toggleIpVisibility" }}
|
||||||
</template>
|
</template>
|
||||||
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
|
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" class="fs-1rem" @click="showIp = !showIp"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
|
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
|
||||||
@@ -365,8 +296,8 @@
|
|||||||
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
|
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
|
||||||
<a-collapse default-active-key="1">
|
<a-collapse default-active-key="1">
|
||||||
<a-collapse-panel key="1" header='Xray'>
|
<a-collapse-panel key="1" header='Xray'>
|
||||||
<a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
|
<a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
|
||||||
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
|
<a-list class="ant-version-list w-100" bordered>
|
||||||
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
|
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
|
||||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
|
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
|
||||||
<a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
|
<a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
|
||||||
@@ -374,15 +305,13 @@
|
|||||||
</a-list>
|
</a-list>
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
<a-collapse-panel key="2" header='Geofiles'>
|
<a-collapse-panel key="2" header='Geofiles'>
|
||||||
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
|
<a-list class="ant-version-list w-100" bordered>
|
||||||
<a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
|
<a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
|
||||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
|
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
|
||||||
<a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
|
<a-icon type="reload" @click="updateGeofile(file)" class="mr-8"/>
|
||||||
</a-list-item>
|
</a-list-item>
|
||||||
</a-list>
|
</a-list>
|
||||||
<div style="margin-top: 5px; display: flex; justify-content: flex-end;">
|
<div class="mt-5 d-flex justify-end"><a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button></div>
|
||||||
<a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button>
|
|
||||||
</div>
|
|
||||||
</a-collapse-panel>
|
</a-collapse-panel>
|
||||||
</a-collapse>
|
</a-collapse>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
@@ -394,15 +323,15 @@
|
|||||||
{{ i18n "pages.index.logs" }}
|
{{ i18n "pages.index.logs" }}
|
||||||
<a-icon :spin="logModal.loading"
|
<a-icon :spin="logModal.loading"
|
||||||
type="sync"
|
type="sync"
|
||||||
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
|
class="va-middle ml-10"
|
||||||
:disabled="logModal.loading"
|
:disabled="logModal.loading"
|
||||||
@click="openLogs()">
|
@click="openLogs()">
|
||||||
</a-icon>
|
</a-icon>
|
||||||
</template>
|
</template>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item :style="{ marginRight: '0.5rem' }">
|
<a-form-item class="mr-05">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }"
|
<a-select size="small" v-model="logModal.rows" class="w-70"
|
||||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="10">10</a-select-option>
|
<a-select-option value="10">10</a-select-option>
|
||||||
<a-select-option value="20">20</a-select-option>
|
<a-select-option value="20">20</a-select-option>
|
||||||
@@ -410,7 +339,7 @@
|
|||||||
<a-select-option value="100">100</a-select-option>
|
<a-select-option value="100">100</a-select-option>
|
||||||
<a-select-option value="500">500</a-select-option>
|
<a-select-option value="500">500</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
<a-select size="small" v-model="logModal.level" :style="{ width: '95px' }"
|
<a-select size="small" v-model="logModal.level" class="w-95"
|
||||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="debug">Debug</a-select-option>
|
<a-select-option value="debug">Debug</a-select-option>
|
||||||
<a-select-option value="info">Info</a-select-option>
|
<a-select-option value="info">Info</a-select-option>
|
||||||
@@ -423,11 +352,11 @@
|
|||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :style="{ float: 'right' }">
|
<a-form-item style="float: right;">
|
||||||
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
|
<div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<a-modal id="xraylog-modal"
|
<a-modal id="xraylog-modal"
|
||||||
v-model="xraylogModal.visible"
|
v-model="xraylogModal.visible"
|
||||||
@@ -439,15 +368,15 @@
|
|||||||
{{ i18n "pages.index.logs" }}
|
{{ i18n "pages.index.logs" }}
|
||||||
<a-icon :spin="xraylogModal.loading"
|
<a-icon :spin="xraylogModal.loading"
|
||||||
type="sync"
|
type="sync"
|
||||||
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
|
class="va-middle ml-10"
|
||||||
:disabled="xraylogModal.loading"
|
:disabled="xraylogModal.loading"
|
||||||
@click="openXrayLogs()">
|
@click="openXrayLogs()">
|
||||||
</a-icon>
|
</a-icon>
|
||||||
</template>
|
</template>
|
||||||
<a-form layout="inline">
|
<a-form layout="inline">
|
||||||
<a-form-item :style="{ marginRight: '0.5rem' }">
|
<a-form-item class="mr-05">
|
||||||
<a-input-group compact>
|
<a-input-group compact>
|
||||||
<a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }"
|
<a-select size="small" v-model="xraylogModal.rows" class="w-70"
|
||||||
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option value="10">10</a-select-option>
|
<a-select-option value="10">10</a-select-option>
|
||||||
<a-select-option value="20">20</a-select-option>
|
<a-select-option value="20">20</a-select-option>
|
||||||
@@ -465,11 +394,11 @@
|
|||||||
<a-checkbox v-model="xraylogModal.showBlocked" @change="openXrayLogs()">Blocked</a-checkbox>
|
<a-checkbox v-model="xraylogModal.showBlocked" @change="openXrayLogs()">Blocked</a-checkbox>
|
||||||
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
|
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item :style="{ float: 'right' }">
|
<a-form-item style="float: right;">
|
||||||
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
</a-form>
|
</a-form>
|
||||||
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="xraylogModal.formattedLogs"></div>
|
<div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div>
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<a-modal id="backup-modal"
|
<a-modal id="backup-modal"
|
||||||
v-model="backupModal.visible"
|
v-model="backupModal.visible"
|
||||||
@@ -477,7 +406,7 @@
|
|||||||
:closable="true"
|
:closable="true"
|
||||||
footer=""
|
footer=""
|
||||||
:class="themeSwitcher.currentTheme">
|
:class="themeSwitcher.currentTheme">
|
||||||
<a-list class="ant-backup-list" bordered :style="{ width: '100%' }">
|
<a-list class="ant-backup-list w-100" bordered>
|
||||||
<a-list-item class="ant-backup-list-item">
|
<a-list-item class="ant-backup-list-item">
|
||||||
<a-list-item-meta>
|
<a-list-item-meta>
|
||||||
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
|
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
|
||||||
@@ -746,7 +675,7 @@ ${dateTime}
|
|||||||
},
|
},
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/server/status');
|
const msg = await HttpUtil.get('/panel/api/server/status');
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
if (!this.loadingStates.fetched) {
|
if (!this.loadingStates.fetched) {
|
||||||
this.loadingStates.fetched = true;
|
this.loadingStates.fetched = true;
|
||||||
@@ -763,7 +692,7 @@ ${dateTime}
|
|||||||
},
|
},
|
||||||
async openSelectV2rayVersion() {
|
async openSelectV2rayVersion() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/getXrayVersion');
|
const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
@@ -780,7 +709,7 @@ ${dateTime}
|
|||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
versionModal.hide();
|
versionModal.hide();
|
||||||
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
||||||
await HttpUtil.post(`/server/installXray/${version}`);
|
await HttpUtil.post(`/panel/api/server/installXray/${version}`);
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -799,8 +728,8 @@ ${dateTime}
|
|||||||
versionModal.hide();
|
versionModal.hide();
|
||||||
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
||||||
const url = isSingleFile
|
const url = isSingleFile
|
||||||
? `/server/updateGeofile/${fileName}`
|
? `/panel/api/server/updateGeofile/${fileName}`
|
||||||
: `/server/updateGeofile`;
|
: `/panel/api/server/updateGeofile`;
|
||||||
await HttpUtil.post(url);
|
await HttpUtil.post(url);
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
},
|
},
|
||||||
@@ -808,7 +737,7 @@ ${dateTime}
|
|||||||
},
|
},
|
||||||
async stopXrayService() {
|
async stopXrayService() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/stopXrayService');
|
const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
@@ -816,7 +745,7 @@ ${dateTime}
|
|||||||
},
|
},
|
||||||
async restartXrayService() {
|
async restartXrayService() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/restartXrayService');
|
const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
@@ -824,7 +753,7 @@ ${dateTime}
|
|||||||
},
|
},
|
||||||
async openLogs(){
|
async openLogs(){
|
||||||
logModal.loading = true;
|
logModal.loading = true;
|
||||||
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
const msg = await HttpUtil.post('/panel/api/server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -834,7 +763,7 @@ ${dateTime}
|
|||||||
},
|
},
|
||||||
async openXrayLogs(){
|
async openXrayLogs(){
|
||||||
xraylogModal.loading = true;
|
xraylogModal.loading = true;
|
||||||
const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
|
const msg = await HttpUtil.post('/panel/api/server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -844,7 +773,7 @@ ${dateTime}
|
|||||||
},
|
},
|
||||||
async openConfig() {
|
async openConfig() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post('server/getConfigJson');
|
const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
@@ -855,7 +784,7 @@ ${dateTime}
|
|||||||
backupModal.show();
|
backupModal.show();
|
||||||
},
|
},
|
||||||
exportDatabase() {
|
exportDatabase() {
|
||||||
window.location = basePath + 'server/getDb';
|
window.location = basePath + 'panel/api/server/getDb';
|
||||||
},
|
},
|
||||||
importDatabase() {
|
importDatabase() {
|
||||||
const fileInput = document.createElement('input');
|
const fileInput = document.createElement('input');
|
||||||
@@ -868,7 +797,7 @@ ${dateTime}
|
|||||||
formData.append('db', dbFile);
|
formData.append('db', dbFile);
|
||||||
backupModal.hide();
|
backupModal.hide();
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
|
const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'multipart/form-data',
|
'Content-Type': 'multipart/form-data',
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,456 +1,10 @@
|
|||||||
{{ template "page/head_start" .}}
|
{{ template "page/head_start" .}}
|
||||||
<style>
|
|
||||||
html * {
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
-moz-osx-font-smoothing: grayscale;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
text-align: center;
|
|
||||||
/*margin: 20px 0 50px 0;*/
|
|
||||||
height: 110px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-form-item-children .ant-btn,
|
|
||||||
.ant-input {
|
|
||||||
height: 50px;
|
|
||||||
border-radius: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-input-group-addon {
|
|
||||||
border-radius: 0 30px 30px 0;
|
|
||||||
width: 50px;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-input-affix-wrapper .ant-input-prefix {
|
|
||||||
left: 23px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-input-affix-wrapper .ant-input:not(:first-child) {
|
|
||||||
padding-left: 50px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.centered {
|
|
||||||
display: flex;
|
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title {
|
|
||||||
font-size: 2rem;
|
|
||||||
margin-block-end: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title b {
|
|
||||||
font-weight: bold !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
#app {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login {
|
|
||||||
animation: charge 0.5s both;
|
|
||||||
background-color: #fff;
|
|
||||||
border-radius: 2rem;
|
|
||||||
padding: 4rem 3rem;
|
|
||||||
transition: all 0.3s;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
-moz-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#login:hover {
|
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes charge {
|
|
||||||
from {
|
|
||||||
transform: translateY(5rem);
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
to {
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.under {
|
|
||||||
background-color: #c7ebe2;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .under {
|
|
||||||
background-color: var(--dark-color-login-wave);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark #login {
|
|
||||||
background-color: var(--dark-color-surface-100);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark h1 {
|
|
||||||
color: rgba(255, 255, 255);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-btn-primary-login {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-btn-primary-login:focus,
|
|
||||||
.ant-btn-primary-login:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #006655;
|
|
||||||
border-color: #006655;
|
|
||||||
background-image: linear-gradient(270deg,
|
|
||||||
rgba(123, 199, 77, 0) 30%,
|
|
||||||
#009980,
|
|
||||||
rgba(123, 199, 77, 0) 100%);
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
animation: ma-bg-move ease-in-out 5s infinite;
|
|
||||||
background-position-x: -500px;
|
|
||||||
width: 95%;
|
|
||||||
animation-delay: -0.5s;
|
|
||||||
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-btn-primary-login.active,
|
|
||||||
.ant-btn-primary-login:active {
|
|
||||||
color: #fff;
|
|
||||||
background-color: #006655;
|
|
||||||
border-color: #006655;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes ma-bg-move {
|
|
||||||
0% {
|
|
||||||
background-position: -500px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
background-position: 1000px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
background-position: 1000px 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.wave-btn-bg {
|
|
||||||
position: relative;
|
|
||||||
border-radius: 25px;
|
|
||||||
width: 100%;
|
|
||||||
transition: all 0.3s cubic-bezier(.645, .045, .355, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .wave-btn-bg {
|
|
||||||
color: #fff;
|
|
||||||
position: relative;
|
|
||||||
background-color: #0a7557;
|
|
||||||
border: 2px double transparent;
|
|
||||||
background-origin: border-box;
|
|
||||||
background-clip: padding-box, border-box;
|
|
||||||
background-size: 300%;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .wave-btn-bg:hover {
|
|
||||||
animation: wave-btn-tara 4s ease infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .wave-btn-bg-cl {
|
|
||||||
background-image: linear-gradient(rgba(13, 14, 33, 0), rgba(13, 14, 33, 0)),
|
|
||||||
radial-gradient(circle at left top, #006655, #009980, #006655) !important;
|
|
||||||
border-radius: 3em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .wave-btn-bg-cl:hover {
|
|
||||||
width: 95%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .wave-btn-bg-cl:before {
|
|
||||||
position: absolute;
|
|
||||||
content: "";
|
|
||||||
top: -5px;
|
|
||||||
left: -5px;
|
|
||||||
bottom: -5px;
|
|
||||||
right: -5px;
|
|
||||||
z-index: -1;
|
|
||||||
background: inherit;
|
|
||||||
background-size: inherit;
|
|
||||||
border-radius: 4em;
|
|
||||||
opacity: 0;
|
|
||||||
transition: 0.5s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .wave-btn-bg-cl:hover::before {
|
|
||||||
opacity: 1;
|
|
||||||
filter: blur(20px);
|
|
||||||
animation: wave-btn-tara 8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes wave-btn-tara {
|
|
||||||
to {
|
|
||||||
background-position: 300%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .ant-btn-primary-login {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
background-image: linear-gradient(rgba(13, 14, 33, 0.45),
|
|
||||||
rgba(13, 14, 33, 0.35));
|
|
||||||
border-radius: 2rem;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
background-color: transparent;
|
|
||||||
height: 46px;
|
|
||||||
position: relative;
|
|
||||||
white-space: nowrap;
|
|
||||||
cursor: pointer;
|
|
||||||
touch-action: manipulation;
|
|
||||||
padding: 0 15px;
|
|
||||||
width: 100%;
|
|
||||||
animation: none;
|
|
||||||
background-position-x: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waves-header {
|
|
||||||
position: fixed;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
background-color: #dbf5ed;
|
|
||||||
color: white;
|
|
||||||
z-index: -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .waves-header {
|
|
||||||
background-color: var(--dark-color-login-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.waves-inner-header {
|
|
||||||
height: 50vh;
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.waves {
|
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 15vh;
|
|
||||||
margin-bottom: -8px;
|
|
||||||
/*Fix for safari gap*/
|
|
||||||
min-height: 100px;
|
|
||||||
max-height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use {
|
|
||||||
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dark .parallax>use {
|
|
||||||
fill: var(--dark-color-login-wave);
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(1) {
|
|
||||||
animation-delay: -2s;
|
|
||||||
animation-duration: 4s;
|
|
||||||
opacity: 0.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(2) {
|
|
||||||
animation-delay: -3s;
|
|
||||||
animation-duration: 7s;
|
|
||||||
opacity: 0.4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(3) {
|
|
||||||
animation-delay: -4s;
|
|
||||||
animation-duration: 10s;
|
|
||||||
opacity: 0.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parallax>use:nth-child(4) {
|
|
||||||
animation-delay: -5s;
|
|
||||||
animation-duration: 13s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes move-forever {
|
|
||||||
0% {
|
|
||||||
transform: translate3d(-90px, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
transform: translate3d(85px, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.waves {
|
|
||||||
height: 40px;
|
|
||||||
min-height: 40px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.words-wrapper {
|
|
||||||
width: 100%;
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.words-wrapper b {
|
|
||||||
width: 100%;
|
|
||||||
display: inline-block;
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
top: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.words-wrapper b.is-visible {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline.zoom .words-wrapper {
|
|
||||||
-webkit-perspective: 300px;
|
|
||||||
-moz-perspective: 300px;
|
|
||||||
perspective: 300px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline.zoom b {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline.zoom b.is-visible {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-animation: zoom-in 0.8s;
|
|
||||||
-moz-animation: zoom-in 0.8s;
|
|
||||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-in 0.8s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.headline.zoom b.is-hidden {
|
|
||||||
-webkit-animation: zoom-out 0.8s;
|
|
||||||
-moz-animation: zoom-out 0.8s;
|
|
||||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-out 0.4s;
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes zoom-in {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateZ(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-moz-keyframes zoom-in {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
-moz-transform: translateZ(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: translateZ(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes zoom-in {
|
|
||||||
0% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateZ(100px);
|
|
||||||
-moz-transform: translateZ(100px);
|
|
||||||
-ms-transform: translateZ(100px);
|
|
||||||
-o-transform: translateZ(100px);
|
|
||||||
transform: translateZ(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
-moz-transform: translateZ(0);
|
|
||||||
-ms-transform: translateZ(0);
|
|
||||||
-o-transform: translateZ(0);
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-webkit-keyframes zoom-out {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateZ(-100px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@-moz-keyframes zoom-out {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
-moz-transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
-moz-transform: translateZ(-100px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes zoom-out {
|
|
||||||
0% {
|
|
||||||
opacity: 1;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
-moz-transform: translateZ(0);
|
|
||||||
-ms-transform: translateZ(0);
|
|
||||||
-o-transform: translateZ(0);
|
|
||||||
transform: translateZ(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
opacity: 0;
|
|
||||||
-webkit-transform: translateZ(-100px);
|
|
||||||
-moz-transform: translateZ(-100px);
|
|
||||||
-ms-transform: translateZ(-100px);
|
|
||||||
-o-transform: translateZ(-100px);
|
|
||||||
transform: translateZ(-100px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.setting-section {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
padding: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-space-item .ant-switch {
|
|
||||||
margin: 2px 0 4px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-layout-content class="under" :style="{ minHeight: '0' }">
|
<a-layout-content class="under min-h-100vh">
|
||||||
<div class="waves-header">
|
<div class="waves-header">
|
||||||
<div class="waves-inner-header"></div>
|
<div class="waves-inner-header"></div>
|
||||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
@@ -466,71 +20,80 @@
|
|||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
|
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-hidden-auto">
|
||||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
|
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
||||||
<div class="setting-section">
|
<template v-if="!loadingStates.fetched">
|
||||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click">
|
<div class="text-center">
|
||||||
<template slot="content">
|
<a-spin size="large" />
|
||||||
<a-space direction="vertical" :size="10">
|
</div>
|
||||||
<a-theme-switch-login></a-theme-switch-login>
|
</template>
|
||||||
<span>{{ i18n "pages.settings.language" }}</span>
|
<template v-else>
|
||||||
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
<div class="setting-section">
|
||||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
||||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
placement="bottomRight" trigger="click">
|
||||||
<span v-text="l.name"></span>
|
<template slot="content">
|
||||||
</a-select-option>
|
<a-space direction="vertical" :size="10">
|
||||||
</a-select>
|
<a-theme-switch-login></a-theme-switch-login>
|
||||||
</a-space>
|
<span>{{ i18n "pages.settings.language" }}</span>
|
||||||
</template>
|
<a-select ref="selectLang" class="w-100" v-model="lang"
|
||||||
<a-button shape="circle" icon="setting"></a-button>
|
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
</a-popover>
|
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||||
</div>
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||||
<a-row type="flex" justify="center">
|
<span v-text="l.name"></span>
|
||||||
<a-col :style="{ width: '100%' }">
|
</a-select-option>
|
||||||
<h2 class="title headline zoom">
|
</a-select>
|
||||||
<span class="words-wrapper">
|
</a-space>
|
||||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
</template>
|
||||||
<b>{{ i18n "pages.login.title" }}</b>
|
<a-button shape="circle" icon="setting"></a-button>
|
||||||
</span>
|
</a-popover>
|
||||||
</h2>
|
</div>
|
||||||
</a-col>
|
<a-row type="flex" justify="center">
|
||||||
</a-row>
|
<a-col :style="{ width: '100%' }">
|
||||||
<a-row type="flex" justify="center">
|
<h2 class="title headline zoom">
|
||||||
<a-col span="24">
|
<span class="words-wrapper">
|
||||||
<a-form>
|
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||||
<a-space direction="vertical" size="middle">
|
<b>{{ i18n "pages.login.title" }}</b>
|
||||||
<a-form-item>
|
</span>
|
||||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
</h2>
|
||||||
placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus>
|
</a-col>
|
||||||
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
|
</a-row>
|
||||||
</a-input>
|
<a-row type="flex" justify="center">
|
||||||
</a-form-item>
|
<a-col span="24">
|
||||||
<a-form-item>
|
<a-form @submit.prevent="login">
|
||||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
<a-space direction="vertical" size="middle">
|
||||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
<a-form-item>
|
||||||
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
|
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||||
</a-input-password>
|
placeholder='{{ i18n "username" }}' autofocus required>
|
||||||
</a-form-item>
|
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
||||||
<a-form-item v-if="twoFactorEnable">
|
</a-input>
|
||||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
</a-form-item>
|
||||||
placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
|
<a-form-item>
|
||||||
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
|
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||||
</a-input>
|
placeholder='{{ i18n "password" }}' required>
|
||||||
</a-form-item>
|
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||||
<a-form-item>
|
</a-input-password>
|
||||||
<a-row justify="center" class="centered">
|
</a-form-item>
|
||||||
<div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl">
|
<a-form-item v-if="twoFactorEnable">
|
||||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
|
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||||
:icon="loading ? 'poweroff' : undefined">
|
placeholder='{{ i18n "twoFactorCode" }}' required>
|
||||||
[[ loading ? '' : '{{ i18n "login" }}' ]]
|
<a-icon slot="prefix" type="key" class="fs-1rem"></a-icon>
|
||||||
</a-button>
|
</a-input>
|
||||||
</div>
|
</a-form-item>
|
||||||
</a-row>
|
<a-form-item>
|
||||||
</a-form-item>
|
<a-row justify="center" class="centered">
|
||||||
</a-space>
|
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
||||||
</a-form>
|
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||||
</a-col>
|
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||||
</a-row>
|
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</a-row>
|
||||||
|
</a-form-item>
|
||||||
|
</a-space>
|
||||||
|
</a-form>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</template>
|
||||||
</a-col>
|
</a-col>
|
||||||
</a-row>
|
</a-row>
|
||||||
</a-layout-content>
|
</a-layout-content>
|
||||||
@@ -544,7 +107,10 @@
|
|||||||
el: '#app',
|
el: '#app',
|
||||||
data: {
|
data: {
|
||||||
themeSwitcher,
|
themeSwitcher,
|
||||||
loading: false,
|
loadingStates: {
|
||||||
|
fetched: false,
|
||||||
|
spinning: false
|
||||||
|
},
|
||||||
user: {
|
user: {
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -559,19 +125,23 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async login() {
|
async login() {
|
||||||
this.loading = true;
|
this.loadingStates.spinning = true;
|
||||||
|
|
||||||
const msg = await HttpUtil.post('/login', this.user);
|
const msg = await HttpUtil.post('/login', this.user);
|
||||||
this.loading = false;
|
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
location.href = basePath + 'panel/';
|
location.href = basePath + 'panel/';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.loadingStates.spinning = false;
|
||||||
},
|
},
|
||||||
async getTwoFactorEnable() {
|
async getTwoFactorEnable() {
|
||||||
this.loading = true;
|
|
||||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||||
this.loading = false;
|
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
this.twoFactorEnable = msg.obj;
|
this.twoFactorEnable = msg.obj;
|
||||||
|
this.loadingStates.fetched = true;
|
||||||
|
|
||||||
return msg.obj;
|
return msg.obj;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -121,7 +121,7 @@
|
|||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
async getDBClientIps(email) {
|
async getDBClientIps(email) {
|
||||||
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
|
const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
document.getElementById("clientIPs").value = msg.obj;
|
document.getElementById("clientIPs").value = msg.obj;
|
||||||
return;
|
return;
|
||||||
@@ -139,7 +139,7 @@
|
|||||||
},
|
},
|
||||||
async clearDBClientIps(email) {
|
async clearDBClientIps(email) {
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
|
const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@
|
|||||||
cancelText: '{{ i18n "cancel"}}',
|
cancelText: '{{ i18n "cancel"}}',
|
||||||
onOk: async () => {
|
onOk: async () => {
|
||||||
iconElement.disabled = true;
|
iconElement.disabled = true;
|
||||||
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
|
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
this.clientModal.clientStats.up = 0;
|
this.clientModal.clientStats.up = 0;
|
||||||
this.clientModal.clientStats.down = 0;
|
this.clientModal.clientStats.down = 0;
|
||||||
|
|||||||
@@ -101,6 +101,12 @@
|
|||||||
{{ i18n "security" }}
|
{{ i18n "security" }}
|
||||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||||
<br />
|
<br />
|
||||||
|
<td>Authentication</td>
|
||||||
|
<a-tag :color="inbound.settings.selectedAuth ? 'green' : 'red'">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
|
||||||
|
<br />
|
||||||
|
{{ i18n "encryption" }}
|
||||||
|
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
||||||
|
<br />
|
||||||
<template v-if="inbound.stream.security != 'none'">
|
<template v-if="inbound.stream.security != 'none'">
|
||||||
{{ i18n "domainName" }}
|
{{ i18n "domainName" }}
|
||||||
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||||
@@ -185,6 +191,44 @@
|
|||||||
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.createdAt" }}</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
|
||||||
|
<template v-if="app.datepicker === 'gregorian'">
|
||||||
|
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-tag>-</a-tag>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "pages.inbounds.updatedAt" }}</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
|
||||||
|
<template v-if="app.datepicker === 'gregorian'">
|
||||||
|
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-tag>-</a-tag>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{{ i18n "lastOnline" }}</td>
|
||||||
|
<td>
|
||||||
|
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr v-if="infoModal.clientSettings.comment">
|
<tr v-if="infoModal.clientSettings.comment">
|
||||||
<td>{{ i18n "comment" }}</td>
|
<td>{{ i18n "comment" }}</td>
|
||||||
<td>
|
<td>
|
||||||
@@ -310,7 +354,7 @@
|
|||||||
<code>[[ link.link ]]</code>
|
<code>[[ link.link ]]</code>
|
||||||
</tr-info-row>
|
</tr-info-row>
|
||||||
</template>
|
</template>
|
||||||
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table">
|
<table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
||||||
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
||||||
@@ -332,7 +376,7 @@
|
|||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
<table v-if="dbInbound.isSocks" class="tr-info-table">
|
<table v-if="dbInbound.isMixed" class="tr-info-table">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ i18n "password" }} Auth</th>
|
<th>{{ i18n "password" }} Auth</th>
|
||||||
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
||||||
@@ -448,7 +492,7 @@
|
|||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
function refreshIPs(email) {
|
function refreshIPs(email) {
|
||||||
return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => {
|
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(msg.obj).join(', ');
|
return JSON.parse(msg.obj).join(', ');
|
||||||
@@ -569,7 +613,7 @@
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
clearClientIps() {
|
clearClientIps() {
|
||||||
HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`)
|
HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`)
|
||||||
.then((msg) => {
|
.then((msg) => {
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
{{define "modals/inboundModal"}}
|
{{define "modals/inboundModal"}}
|
||||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title"
|
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
|
||||||
:dialog-style="{ top: '20px' }" @ok="inModal.ok"
|
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||||
:class="themeSwitcher.currentTheme"
|
|
||||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
|
||||||
{{template "form/inbound"}}
|
{{template "form/inbound"}}
|
||||||
</a-modal>
|
</a-modal>
|
||||||
<script>
|
<script>
|
||||||
@@ -20,7 +18,7 @@
|
|||||||
ok() {
|
ok() {
|
||||||
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
||||||
},
|
},
|
||||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
|
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
|
||||||
this.title = title;
|
this.title = title;
|
||||||
this.okText = okText;
|
this.okText = okText;
|
||||||
if (inbound) {
|
if (inbound) {
|
||||||
@@ -41,7 +39,7 @@
|
|||||||
inModal.visible = false;
|
inModal.visible = false;
|
||||||
inModal.loading(false);
|
inModal.loading(false);
|
||||||
},
|
},
|
||||||
loading(loading=true) {
|
loading(loading = true) {
|
||||||
inModal.confirmLoading = loading;
|
inModal.confirmLoading = loading;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -107,7 +105,7 @@
|
|||||||
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||||
|
|
||||||
if (this.inModal.inbound.isSSMultiUser) {
|
if (this.inModal.inbound.isSSMultiUser) {
|
||||||
if (this.inModal.inbound.settings.shadowsockses.length ==0){
|
if (this.inModal.inbound.settings.shadowsockses.length == 0) {
|
||||||
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||||
}
|
}
|
||||||
if (!this.inModal.inbound.isSS2022) {
|
if (!this.inModal.inbound.isSS2022) {
|
||||||
@@ -123,7 +121,7 @@
|
|||||||
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
if (this.inModal.inbound.settings.shadowsockses.length > 0){
|
if (this.inModal.inbound.settings.shadowsockses.length > 0) {
|
||||||
this.inModal.inbound.settings.shadowsockses = [];
|
this.inModal.inbound.settings.shadowsockses = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +132,7 @@
|
|||||||
},
|
},
|
||||||
async getNewX25519Cert() {
|
async getNewX25519Cert() {
|
||||||
inModal.loading(true);
|
inModal.loading(true);
|
||||||
const msg = await HttpUtil.post('/server/getNewX25519Cert');
|
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
|
||||||
inModal.loading(false);
|
inModal.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
@@ -142,9 +140,13 @@
|
|||||||
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
||||||
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
||||||
},
|
},
|
||||||
|
clearX25519Cert() {
|
||||||
|
this.inbound.stream.reality.privateKey = '';
|
||||||
|
this.inbound.stream.reality.settings.publicKey = '';
|
||||||
|
},
|
||||||
async getNewmldsa65() {
|
async getNewmldsa65() {
|
||||||
inModal.loading(true);
|
inModal.loading(true);
|
||||||
const msg = await HttpUtil.post('/server/getNewmldsa65');
|
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
|
||||||
inModal.loading(false);
|
inModal.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
@@ -152,9 +154,13 @@
|
|||||||
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
|
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
|
||||||
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
|
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
|
||||||
},
|
},
|
||||||
|
clearMldsa65() {
|
||||||
|
this.inbound.stream.reality.mldsa65Seed = '';
|
||||||
|
this.inbound.stream.reality.settings.mldsa65Verify = '';
|
||||||
|
},
|
||||||
async getNewEchCert() {
|
async getNewEchCert() {
|
||||||
inModal.loading(true);
|
inModal.loading(true);
|
||||||
const msg = await HttpUtil.post('/server/getNewEchCert', {sni: inModal.inbound.stream.tls.sni});
|
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
|
||||||
inModal.loading(false);
|
inModal.loading(false);
|
||||||
if (!msg.success) {
|
if (!msg.success) {
|
||||||
return;
|
return;
|
||||||
@@ -162,6 +168,37 @@
|
|||||||
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
|
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
|
||||||
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
|
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
|
||||||
},
|
},
|
||||||
|
clearEchCert() {
|
||||||
|
this.inbound.stream.tls.echServerKeys = '';
|
||||||
|
this.inbound.stream.tls.settings.echConfigList = '';
|
||||||
|
},
|
||||||
|
async getNewVlessEnc() {
|
||||||
|
inModal.loading(true);
|
||||||
|
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
|
||||||
|
inModal.loading(false);
|
||||||
|
|
||||||
|
if (!msg.success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auths = msg.obj.auths || [];
|
||||||
|
const selected = inModal.inbound.settings.selectedAuth;
|
||||||
|
const block = auths.find(a => a.label === selected);
|
||||||
|
|
||||||
|
if (!block) {
|
||||||
|
console.error("No auth block for", selected);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
inModal.inbound.settings.decryption = block.decryption;
|
||||||
|
inModal.inbound.settings.encryption = block.encryption;
|
||||||
|
},
|
||||||
|
clearVlessEnc() {
|
||||||
|
this.inbound.settings.decryption = 'none';
|
||||||
|
this.inbound.settings.encryption = 'none';
|
||||||
|
this.inbound.settings.selectedAuth = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -151,7 +151,7 @@
|
|||||||
methods: {
|
methods: {
|
||||||
async getStatus() {
|
async getStatus() {
|
||||||
try {
|
try {
|
||||||
const msg = await HttpUtil.post('/server/status');
|
const msg = await HttpUtil.get('/panel/api/server/status');
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
this.serverStatus = msg.obj;
|
this.serverStatus = msg.obj;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
{{define "modals/ruleModal"}}
|
{{define "modals/ruleModal"}}
|
||||||
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||||
<a-form-item label='Domain Matcher'>
|
|
||||||
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
|
|
||||||
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
|
|
||||||
</a-select>
|
|
||||||
</a-form-item>
|
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
@@ -14,7 +9,7 @@
|
|||||||
</template> Source IPs <a-icon type="question-circle"></a-icon>
|
</template> Source IPs <a-icon type="question-circle"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="ruleModal.rule.source"></a-input>
|
<a-input v-model.trim="ruleModal.rule.sourceIP" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
@@ -24,7 +19,17 @@
|
|||||||
</template> Source Port <a-icon type="question-circle"></a-icon>
|
</template> Source Port <a-icon type="question-circle"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input>
|
<a-input v-model.trim="ruleModal.rule.sourcePort" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||||
|
</a-form-item>
|
||||||
|
<a-form-item>
|
||||||
|
<template slot="label">
|
||||||
|
<a-tooltip>
|
||||||
|
<template slot="title">
|
||||||
|
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||||
|
</template> VLESS Route <a-icon type="question-circle"></a-icon>
|
||||||
|
</a-tooltip>
|
||||||
|
</template>
|
||||||
|
<a-input v-model.trim="ruleModal.rule.vlessRoute" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='Network'>
|
<a-form-item label='Network'>
|
||||||
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
@@ -57,7 +62,7 @@
|
|||||||
</template> IP <a-icon type="question-circle"></a-icon>
|
</template> IP <a-icon type="question-circle"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="ruleModal.rule.ip"></a-input>
|
<a-input v-model.trim="ruleModal.rule.ip" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
@@ -67,7 +72,7 @@
|
|||||||
</template> Domain <a-icon type="question-circle"></a-icon>
|
</template> Domain <a-icon type="question-circle"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="ruleModal.rule.domain"></a-input>
|
<a-input v-model.trim="ruleModal.rule.domain" placeholder="e.g. google.com, geosite:cn"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
@@ -77,7 +82,7 @@
|
|||||||
</template> User <a-icon type="question-circle"></a-icon>
|
</template> User <a-icon type="question-circle"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="ruleModal.rule.user"></a-input>
|
<a-input v-model.trim="ruleModal.rule.user" placeholder="e.g. email address"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<template slot="label">
|
<template slot="label">
|
||||||
@@ -87,7 +92,7 @@
|
|||||||
</template> Port <a-icon type="question-circle"></a-icon>
|
</template> Port <a-icon type="question-circle"></a-icon>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
</template>
|
</template>
|
||||||
<a-input v-model.trim="ruleModal.rule.port"></a-input>
|
<a-input v-model.trim="ruleModal.rule.port" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item label='Inbound Tags'>
|
<a-form-item label='Inbound Tags'>
|
||||||
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
|
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
@@ -123,13 +128,13 @@
|
|||||||
confirm: null,
|
confirm: null,
|
||||||
rule: {
|
rule: {
|
||||||
type: "field",
|
type: "field",
|
||||||
domainMatcher: "",
|
|
||||||
domain: "",
|
domain: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
port: "",
|
port: "",
|
||||||
sourcePort: "",
|
sourcePort: "",
|
||||||
|
vlessRoute: "",
|
||||||
network: "",
|
network: "",
|
||||||
source: "",
|
sourceIP: "",
|
||||||
user: "",
|
user: "",
|
||||||
inboundTag: [],
|
inboundTag: [],
|
||||||
protocol: [],
|
protocol: [],
|
||||||
@@ -157,13 +162,13 @@
|
|||||||
this.confirm = confirm;
|
this.confirm = confirm;
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
if (isEdit) {
|
if (isEdit) {
|
||||||
this.rule.domainMatcher = rule.domainMatcher;
|
|
||||||
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
|
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
|
||||||
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
|
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
|
||||||
this.rule.port = rule.port;
|
this.rule.port = rule.port;
|
||||||
this.rule.sourcePort = rule.sourcePort;
|
this.rule.sourcePort = rule.sourcePort;
|
||||||
|
this.rule.vlessRoute = rule.vlessRoute;
|
||||||
this.rule.network = rule.network;
|
this.rule.network = rule.network;
|
||||||
this.rule.source = rule.source ? rule.source.join(',') : [];
|
this.rule.sourceIP = rule.sourceIP ? rule.sourceIP.join(',') : [];
|
||||||
this.rule.user = rule.user ? rule.user.join(',') : [];
|
this.rule.user = rule.user ? rule.user.join(',') : [];
|
||||||
this.rule.inboundTag = rule.inboundTag;
|
this.rule.inboundTag = rule.inboundTag;
|
||||||
this.rule.protocol = rule.protocol;
|
this.rule.protocol = rule.protocol;
|
||||||
@@ -172,13 +177,13 @@
|
|||||||
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
|
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
|
||||||
} else {
|
} else {
|
||||||
this.rule = {
|
this.rule = {
|
||||||
domainMatcher: "",
|
|
||||||
domain: "",
|
domain: "",
|
||||||
ip: "",
|
ip: "",
|
||||||
port: "",
|
port: "",
|
||||||
sourcePort: "",
|
sourcePort: "",
|
||||||
|
vlessRoute: "",
|
||||||
network: "",
|
network: "",
|
||||||
source: "",
|
sourceIP: "",
|
||||||
user: "",
|
user: "",
|
||||||
inboundTag: [],
|
inboundTag: [],
|
||||||
protocol: [],
|
protocol: [],
|
||||||
@@ -214,13 +219,13 @@
|
|||||||
rule = {};
|
rule = {};
|
||||||
newRule = {};
|
newRule = {};
|
||||||
rule.type = "field";
|
rule.type = "field";
|
||||||
rule.domainMatcher = value.domainMatcher;
|
|
||||||
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
|
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
|
||||||
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
|
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
|
||||||
rule.port = value.port;
|
rule.port = value.port;
|
||||||
rule.sourcePort = value.sourcePort;
|
rule.sourcePort = value.sourcePort;
|
||||||
|
rule.vlessRoute = value.vlessRoute;
|
||||||
rule.network = value.network;
|
rule.network = value.network;
|
||||||
rule.source = value.source.length > 0 ? value.source.split(',') : [];
|
rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',') : [];
|
||||||
rule.user = value.user.length > 0 ? value.user.split(',') : [];
|
rule.user = value.user.length > 0 ? value.user.split(',') : [];
|
||||||
rule.inboundTag = value.inboundTag;
|
rule.inboundTag = value.inboundTag;
|
||||||
rule.protocol = value.protocol;
|
rule.protocol = value.protocol;
|
||||||
|
|||||||
@@ -1,67 +1,8 @@
|
|||||||
{{ template "page/head_start" .}}
|
{{ template "page/head_start" .}}
|
||||||
<style>
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.ant-layout-content {
|
|
||||||
margin: 24px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.ant-tabs-nav .ant-tabs-tab {
|
|
||||||
margin: 0;
|
|
||||||
padding: 12px .5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.ant-tabs-bar {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
.ant-list-item {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.alert-msg {
|
|
||||||
color: rgb(194, 117, 18);
|
|
||||||
font-weight: normal;
|
|
||||||
font-size: 16px;
|
|
||||||
padding: .5rem 1rem;
|
|
||||||
text-align: center;
|
|
||||||
background: rgb(255 145 0 / 15%);
|
|
||||||
margin: 1.5rem 2.5rem 0rem;
|
|
||||||
border-radius: .5rem;
|
|
||||||
transition: all 0.5s;
|
|
||||||
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
|
|
||||||
}
|
|
||||||
.alert-msg:hover {
|
|
||||||
cursor: default;
|
|
||||||
transition-duration: .3s;
|
|
||||||
animation: signal 0.9s ease infinite;
|
|
||||||
}
|
|
||||||
@keyframes signal {
|
|
||||||
0% {
|
|
||||||
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
50% {
|
|
||||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
|
||||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.alert-msg>i {
|
|
||||||
color: inherit;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
.dark .ant-input-password-icon {
|
|
||||||
color: var(--dark-color-text-primary);
|
|
||||||
}
|
|
||||||
.ant-collapse-content-box .ant-alert {
|
|
||||||
margin-block-end: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' settings-page'">
|
||||||
<a-sidebar></a-sidebar>
|
<a-sidebar></a-sidebar>
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
|
|||||||
@@ -67,18 +67,22 @@
|
|||||||
</template>
|
</template>
|
||||||
<template slot="info" slot-scope="text, rule, index">
|
<template slot="info" slot-scope="text, rule, index">
|
||||||
<a-popover placement="bottomRight"
|
<a-popover placement="bottomRight"
|
||||||
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
v-if="(rule.sourceIP+rule.sourcePort+rule.vlessRoute+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||||
:overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
:overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||||
<template slot="content">
|
<template slot="content">
|
||||||
<table cellpadding="2" :style="{ maxWidth: '300px' }">
|
<table cellpadding="2" :style="{ maxWidth: '300px' }">
|
||||||
<tr v-if="rule.source">
|
<tr v-if="rule.sourceIP">
|
||||||
<td>Source</td>
|
<td>Source IP</td>
|
||||||
<td><a-tag color="blue" v-for="r in rule.source.split(',')">[[ r ]]</a-tag></td>
|
<td><a-tag color="blue" v-for="r in rule.sourceIP.split(',')">[[ r ]]</a-tag></td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="rule.sourcePort">
|
<tr v-if="rule.sourcePort">
|
||||||
<td>Source Port</td>
|
<td>Source Port</td>
|
||||||
<td><a-tag color="green" v-for="r in rule.sourcePort.split(',')">[[ r ]]</a-tag></td>
|
<td><a-tag color="green" v-for="r in rule.sourcePort.split(',')">[[ r ]]</a-tag></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr v-if="rule.vlessRoute">
|
||||||
|
<td>VLESS Route</td>
|
||||||
|
<td><a-tag color="geekblue" v-for="r in rule.vlessRoute.split(',')">[[ r ]]</a-tag></td>
|
||||||
|
</tr>
|
||||||
<tr v-if="rule.network">
|
<tr v-if="rule.network">
|
||||||
<td>Network</td>
|
<td>Network</td>
|
||||||
<td><a-tag color="blue" v-for="r in rule.network.split(',')">[[ r ]]</a-tag></td>
|
<td><a-tag color="blue" v-for="r in rule.network.split(',')">[[ r ]]</a-tag></td>
|
||||||
|
|||||||
275
web/html/subscription.html
Normal file
275
web/html/subscription.html
Normal file
@@ -0,0 +1,275 @@
|
|||||||
|
{{ template "page/head_start" .}}
|
||||||
|
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
|
||||||
|
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
|
||||||
|
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
|
||||||
|
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
||||||
|
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||||
|
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||||
|
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
||||||
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
|
{{ template "page/body_start" .}}
|
||||||
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||||
|
<a-layout-content class="p-2">
|
||||||
|
<a-row type="flex" justify="center" class="mt-2">
|
||||||
|
<a-col :xs="24" :sm="22" :md="18" :lg="14" :xl="12">
|
||||||
|
<a-card hoverable class="subscription-card">
|
||||||
|
<template #title>
|
||||||
|
<a-space>
|
||||||
|
<span>{{ i18n "subscription.title" }}</span>
|
||||||
|
<a-tag>{{ .sId }}</a-tag>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<template #extra>
|
||||||
|
<a-popover
|
||||||
|
:overlay-class-name="themeSwitcher.currentTheme"
|
||||||
|
title='{{ i18n "menu.settings" }}'
|
||||||
|
placement="bottomRight" trigger="click">
|
||||||
|
<template #content>
|
||||||
|
<a-space direction="vertical" :size="10">
|
||||||
|
<a-theme-switch-login></a-theme-switch-login>
|
||||||
|
<span>{{ i18n "pages.settings.language"
|
||||||
|
}}</span>
|
||||||
|
<a-select ref="selectLang" class="w-100"
|
||||||
|
v-model="lang"
|
||||||
|
@change="LanguageManager.setLanguage(lang)"
|
||||||
|
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
|
<a-select-option :value="l.value"
|
||||||
|
label="English"
|
||||||
|
v-for="l in LanguageManager.supportedLanguages"
|
||||||
|
:key="l.value">
|
||||||
|
<span role="img"
|
||||||
|
:aria-label="l.name"
|
||||||
|
v-text="l.icon"></span>
|
||||||
|
<span
|
||||||
|
v-text="l.name"></span>
|
||||||
|
</a-select-option>
|
||||||
|
</a-select>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
<a-button shape="circle" icon="setting"></a-button>
|
||||||
|
</a-popover>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item>
|
||||||
|
<a-space direction="vertical" align="center">
|
||||||
|
<a-row type="flex" :gutter="[8,8]"
|
||||||
|
justify="center" style="width:100%">
|
||||||
|
<a-col :xs="24" :sm="12"
|
||||||
|
style="text-align:center;">
|
||||||
|
<tr-qr-box class="qr-box">
|
||||||
|
<a-tag color="purple"
|
||||||
|
class="qr-tag">
|
||||||
|
<span>{{ i18n
|
||||||
|
"pages.settings.subSettings"}}</span>
|
||||||
|
</a-tag>
|
||||||
|
<tr-qr-bg class="qr-bg-sub">
|
||||||
|
<tr-qr-bg-inner
|
||||||
|
class="qr-bg-sub-inner">
|
||||||
|
<canvas id="qrcode"
|
||||||
|
class="qr-cv"
|
||||||
|
title='{{ i18n "copy" }}'
|
||||||
|
@click="copy(app.subUrl)"></canvas>
|
||||||
|
</tr-qr-bg-inner>
|
||||||
|
</tr-qr-bg>
|
||||||
|
</tr-qr-box>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="24" :sm="12"
|
||||||
|
style="text-align:center;">
|
||||||
|
<tr-qr-box class="qr-box">
|
||||||
|
<a-tag color="purple"
|
||||||
|
class="qr-tag">
|
||||||
|
<span>{{ i18n
|
||||||
|
"pages.settings.subSettings"}}
|
||||||
|
Json</span>
|
||||||
|
</a-tag>
|
||||||
|
<tr-qr-bg class="qr-bg-sub">
|
||||||
|
<tr-qr-bg-inner
|
||||||
|
class="qr-bg-sub-inner">
|
||||||
|
<canvas id="qrcode-subjson"
|
||||||
|
class="qr-cv"
|
||||||
|
title='{{ i18n "copy" }}'
|
||||||
|
@click="copy(app.subJsonUrl)"></canvas>
|
||||||
|
</tr-qr-bg-inner>
|
||||||
|
</tr-qr-bg>
|
||||||
|
</tr-qr-box>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-space>
|
||||||
|
</a-form-item>
|
||||||
|
|
||||||
|
<a-form-item>
|
||||||
|
<a-descriptions bordered :column="1" size="small">
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "subscription.subId" }}'>[[
|
||||||
|
app.sId
|
||||||
|
]]</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "subscription.status" }}'>
|
||||||
|
<template v-if="isUnlimited">
|
||||||
|
<a-tag color="purple">{{ i18n
|
||||||
|
"subscription.unlimited" }}</a-tag>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<a-tag
|
||||||
|
:color="isActive ? 'green' : 'red'">[[
|
||||||
|
isActive ? '{{ i18n
|
||||||
|
"subscription.active" }}' : '{{ i18n
|
||||||
|
"subscription.inactive" }}'
|
||||||
|
]]</a-tag>
|
||||||
|
</template>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "subscription.downloaded" }}'>[[
|
||||||
|
app.download
|
||||||
|
]]</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "subscription.uploaded" }}'>[[
|
||||||
|
app.upload
|
||||||
|
]]</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "usage" }}'>[[ app.used
|
||||||
|
]]</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "subscription.totalQuota" }}'>[[
|
||||||
|
app.total
|
||||||
|
]]</a-descriptions-item>
|
||||||
|
<a-descriptions-item v-if="app.totalByte > 0"
|
||||||
|
label='{{ i18n "remained" }}'>[[
|
||||||
|
app.remained ]]</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "lastOnline" }}'>
|
||||||
|
<template v-if="app.lastOnlineMs > 0">
|
||||||
|
<template
|
||||||
|
v-if="app.datepicker === 'gregorian'">
|
||||||
|
[[
|
||||||
|
DateUtil.formatMillis(app.lastOnlineMs)
|
||||||
|
]]
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
[[
|
||||||
|
DateUtil.convertToJalalian(moment(app.lastOnlineMs))
|
||||||
|
]]
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span>-</span>
|
||||||
|
</template>
|
||||||
|
</a-descriptions-item>
|
||||||
|
<a-descriptions-item
|
||||||
|
label='{{ i18n "subscription.expiry" }}'>
|
||||||
|
<template v-if="app.expireMs === 0">
|
||||||
|
{{ i18n "subscription.noExpiry" }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template
|
||||||
|
v-if="app.datepicker === 'gregorian'">
|
||||||
|
[[
|
||||||
|
DateUtil.formatMillis(app.expireMs)
|
||||||
|
]]
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
[[
|
||||||
|
DateUtil.convertToJalalian(moment(app.expireMs))
|
||||||
|
]]
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</a-descriptions-item>
|
||||||
|
</a-descriptions>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
|
||||||
|
<br />
|
||||||
|
<a-list bordered>
|
||||||
|
<a-list-item v-for="(link, idx) in links" :key="link">
|
||||||
|
<div style="width:100%; text-align:center;">
|
||||||
|
<a-button type="primary" :block="isMobile"
|
||||||
|
@click="copy(link)">[[ linkName(link, idx)
|
||||||
|
]]</a-button>
|
||||||
|
</div>
|
||||||
|
</a-list-item>
|
||||||
|
</a-list>
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<a-form layout="vertical">
|
||||||
|
<a-form-item>
|
||||||
|
<a-row type="flex" justify="center" :gutter="[8,8]"
|
||||||
|
style="width:100%">
|
||||||
|
<a-col :xs="24" :sm="12"
|
||||||
|
style="text-align:center;">
|
||||||
|
<!-- Android dropdown -->
|
||||||
|
<a-dropdown :trigger="['click']">
|
||||||
|
<a-button :block="isMobile"
|
||||||
|
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||||
|
size="large" type="primary">
|
||||||
|
Android <a-icon type="down" />
|
||||||
|
</a-button>
|
||||||
|
<a-menu slot="overlay"
|
||||||
|
:class="themeSwitcher.currentTheme">
|
||||||
|
<a-menu-item key="android-v2box"
|
||||||
|
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
||||||
|
<a-menu-item key="android-v2rayng"
|
||||||
|
@click="open('v2rayng://install-config?url=' + encodeURIComponent(app.subUrl))">V2RayNG</a-menu-item>
|
||||||
|
<a-menu-item key="android-singbox"
|
||||||
|
@click="copy(app.subUrl)">Sing-box</a-menu-item>
|
||||||
|
<a-menu-item key="android-v2raytun"
|
||||||
|
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
||||||
|
<a-menu-item key="android-npvtunnel"
|
||||||
|
@click="copy(app.subUrl)">NPV
|
||||||
|
Tunnel</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
</a-col>
|
||||||
|
<a-col :xs="24" :sm="12"
|
||||||
|
style="text-align:center;">
|
||||||
|
<!-- iOS dropdown -->
|
||||||
|
<a-dropdown :trigger="['click']">
|
||||||
|
<a-button :block="isMobile"
|
||||||
|
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||||
|
size="large" type="primary">
|
||||||
|
iOS <a-icon type="down" />
|
||||||
|
</a-button>
|
||||||
|
<a-menu slot="overlay"
|
||||||
|
:class="themeSwitcher.currentTheme">
|
||||||
|
<a-menu-item key="ios-shadowrocket"
|
||||||
|
@click="open('shadowrocket://add/subscription?url=' + encodeURIComponent(app.subUrl) + '&remark=' + encodeURIComponent(app.sId))">Shadowrocket</a-menu-item>
|
||||||
|
<a-menu-item key="ios-v2box"
|
||||||
|
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
||||||
|
<a-menu-item key="ios-streisand"
|
||||||
|
@click="open('streisand://import/' + encodeURIComponent(app.subUrl))">Streisand</a-menu-item>
|
||||||
|
<a-menu-item key="ios-v2raytun"
|
||||||
|
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
||||||
|
<a-menu-item key="ios-npvtunnel"
|
||||||
|
@click="copy(app.subUrl)">NPV
|
||||||
|
Tunnel</a-menu-item>
|
||||||
|
</a-menu>
|
||||||
|
</a-dropdown>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-form-item>
|
||||||
|
</a-form>
|
||||||
|
</a-card>
|
||||||
|
</a-col>
|
||||||
|
</a-row>
|
||||||
|
</a-layout-content>
|
||||||
|
</a-layout>
|
||||||
|
|
||||||
|
<!-- Bootstrap data for external JS -->
|
||||||
|
<template id="subscription-data" data-sid="{{ .sId }}"
|
||||||
|
data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
|
||||||
|
data-download="{{ .download }}"
|
||||||
|
data-upload="{{ .upload }}" data-used="{{ .used }}"
|
||||||
|
data-total="{{ .total }}" data-remained="{{ .remained }}"
|
||||||
|
data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
|
||||||
|
data-downloadbyte="{{ .downloadByte }}"
|
||||||
|
data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
|
||||||
|
data-datepicker="{{ .datepicker }}"></template>
|
||||||
|
<textarea id="subscription-links"
|
||||||
|
style="display:none">{{ range .result }}{{ . }}
|
||||||
|
{{ end }}</textarea>
|
||||||
|
|
||||||
|
{{template "component/aThemeSwitch" .}}
|
||||||
|
<script src="{{ .base_path }}assets/js/subscription.js?{{ .cur_ver }}"></script>
|
||||||
|
|
||||||
|
{{ template "page/body_end" .}}
|
||||||
@@ -3,45 +3,10 @@
|
|||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
||||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
||||||
<style>
|
|
||||||
@media (min-width: 769px) {
|
|
||||||
.ant-layout-content {
|
|
||||||
margin: 24px 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.ant-tabs-nav .ant-tabs-tab {
|
|
||||||
margin: 0;
|
|
||||||
padding: 12px .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-table-thead>tr>th,
|
|
||||||
.ant-table-tbody>tr>td {
|
|
||||||
padding: 10px 0px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-bar {
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-list-item {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-list-item>li {
|
|
||||||
padding: 10px 20px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-collapse-content-box .ant-alert {
|
|
||||||
margin-block-end: 12px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
{{ template "page/head_end" .}}
|
{{ template "page/head_end" .}}
|
||||||
|
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' xray-page'">
|
||||||
<a-sidebar></a-sidebar>
|
<a-sidebar></a-sidebar>
|
||||||
<a-layout id="content-layout">
|
<a-layout id="content-layout">
|
||||||
<a-layout-content>
|
<a-layout-content>
|
||||||
@@ -181,8 +146,9 @@
|
|||||||
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
|
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
|
||||||
{
|
{
|
||||||
title: '{{ i18n "pages.xray.rules.source"}}', children: [
|
title: '{{ i18n "pages.xray.rules.source"}}', children: [
|
||||||
{ title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true },
|
{ title: 'IP', dataIndex: "sourceIP", align: 'center', width: 20, ellipsis: true },
|
||||||
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }]
|
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true },
|
||||||
|
{ title: 'VLESS Route', dataIndex: 'vlessRoute', align: 'center', width: 15, ellipsis: true }]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '{{ i18n "pages.inbounds.network"}}', children: [
|
title: '{{ i18n "pages.inbounds.network"}}', children: [
|
||||||
@@ -351,7 +317,7 @@
|
|||||||
{ label: '🇨🇳 China', value: 'geosite:cn' },
|
{ label: '🇨🇳 China', value: 'geosite:cn' },
|
||||||
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
|
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
|
||||||
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
|
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
|
||||||
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' },
|
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru$' },
|
||||||
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
|
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
|
||||||
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
|
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
|
||||||
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
|
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
|
||||||
@@ -420,7 +386,7 @@
|
|||||||
},
|
},
|
||||||
async restartXray() {
|
async restartXray() {
|
||||||
this.loading(true);
|
this.loading(true);
|
||||||
const msg = await HttpUtil.post("server/restartXrayService");
|
const msg = await HttpUtil.post("/panel/api/server/restartXrayService");
|
||||||
this.loading(false);
|
this.loading(false);
|
||||||
if (msg.success) {
|
if (msg.success) {
|
||||||
await PromiseUtil.sleep(500);
|
await PromiseUtil.sleep(500);
|
||||||
@@ -572,7 +538,7 @@
|
|||||||
serverObj = o.settings.vnext;
|
serverObj = o.settings.vnext;
|
||||||
break;
|
break;
|
||||||
case Protocols.HTTP:
|
case Protocols.HTTP:
|
||||||
case Protocols.Socks:
|
case Protocols.Mixed:
|
||||||
case Protocols.Shadowsocks:
|
case Protocols.Shadowsocks:
|
||||||
case Protocols.Trojan:
|
case Protocols.Trojan:
|
||||||
serverObj = o.settings.servers;
|
serverObj = o.settings.servers;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -39,12 +40,20 @@ func (j *CheckClientIpJob) Run() {
|
|||||||
f2bInstalled := j.checkFail2BanInstalled()
|
f2bInstalled := j.checkFail2BanInstalled()
|
||||||
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
|
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
|
||||||
|
|
||||||
if iplimitActive {
|
if isAccessLogAvailable {
|
||||||
if f2bInstalled && isAccessLogAvailable {
|
if runtime.GOOS == "windows" {
|
||||||
shouldClearAccessLog = j.processLogFile()
|
if iplimitActive {
|
||||||
|
shouldClearAccessLog = j.processLogFile()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if !f2bInstalled {
|
if iplimitActive {
|
||||||
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
|
if f2bInstalled {
|
||||||
|
shouldClearAccessLog = j.processLogFile()
|
||||||
|
} else {
|
||||||
|
if !f2bInstalled {
|
||||||
|
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package locale
|
|||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
@@ -78,6 +79,11 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
|||||||
|
|
||||||
templateData := createTemplateData(params)
|
templateData := createTemplateData(params)
|
||||||
|
|
||||||
|
if localizer == nil {
|
||||||
|
// Fallback to key if localizer not ready; prevents nil panic on pages like sub
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||||
MessageID: key,
|
MessageID: key,
|
||||||
TemplateData: templateData,
|
TemplateData: templateData,
|
||||||
@@ -102,6 +108,15 @@ func initTGBotLocalizer(settingService SettingService) error {
|
|||||||
|
|
||||||
func LocalizerMiddleware() gin.HandlerFunc {
|
func LocalizerMiddleware() gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
|
// Ensure bundle is initialized so creating a Localizer won't panic
|
||||||
|
if i18nBundle == nil {
|
||||||
|
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||||
|
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||||
|
// Try lazy-load from disk when running sub server without InitLocalizer
|
||||||
|
if err := loadTranslationsFromDisk(i18nBundle); err != nil {
|
||||||
|
logger.Warning("i18n lazy load failed:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
var lang string
|
var lang string
|
||||||
|
|
||||||
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
||||||
@@ -118,6 +133,25 @@ func LocalizerMiddleware() gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// loadTranslationsFromDisk attempts to load translation files from "web/translation" using the local filesystem.
|
||||||
|
func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
|
||||||
|
root := os.DirFS("web")
|
||||||
|
return fs.WalkDir(root, "translation", func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if d.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
data, err := fs.ReadFile(root, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = bundle.ParseMessageFileBytes(data, path)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||||
err := fs.WalkDir(i18nFS, "translation",
|
err := fs.WalkDir(i18nFS, "translation",
|
||||||
func(path string, d fs.DirEntry, err error) error {
|
func(path string, d fs.DirEntry, err error) error {
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"tag": "api",
|
"tag": "api",
|
||||||
"listen": "127.0.0.1",
|
"listen": "127.0.0.1",
|
||||||
"port": 62789,
|
"port": 62789,
|
||||||
"protocol": "dokodemo-door",
|
"protocol": "tunnel",
|
||||||
"settings": {
|
"settings": {
|
||||||
"address": "127.0.0.1"
|
"address": "127.0.0.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -349,6 +349,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||||||
var oldSettings map[string]any
|
var oldSettings map[string]any
|
||||||
_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||||
emailToCreated := map[string]int64{}
|
emailToCreated := map[string]int64{}
|
||||||
|
emailToUpdated := map[string]int64{}
|
||||||
if oldSettings != nil {
|
if oldSettings != nil {
|
||||||
if oc, ok := oldSettings["clients"].([]any); ok {
|
if oc, ok := oldSettings["clients"].([]any); ok {
|
||||||
for _, it := range oc {
|
for _, it := range oc {
|
||||||
@@ -360,6 +361,12 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||||||
case int64:
|
case int64:
|
||||||
emailToCreated[email] = v
|
emailToCreated[email] = v
|
||||||
}
|
}
|
||||||
|
switch v := m["updated_at"].(type) {
|
||||||
|
case float64:
|
||||||
|
emailToUpdated[email] = int64(v)
|
||||||
|
case int64:
|
||||||
|
emailToUpdated[email] = v
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -379,7 +386,12 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||||||
m["created_at"] = now
|
m["created_at"] = now
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m["updated_at"] = now
|
// Preserve client's updated_at if present; do not bump on parent inbound update
|
||||||
|
if _, hasUpdated := m["updated_at"]; !hasUpdated {
|
||||||
|
if v, ok4 := emailToUpdated[email]; ok4 && v > 0 {
|
||||||
|
m["updated_at"] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
nSlice[i] = m
|
nSlice[i] = m
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -967,6 +979,7 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||||||
// Add user in onlineUsers array on traffic
|
// Add user in onlineUsers array on traffic
|
||||||
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
||||||
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
||||||
|
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -2187,6 +2200,20 @@ func (s *InboundService) GetOnlineClients() []string {
|
|||||||
return p.GetOnlineClients()
|
return p.GetOnlineClients()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var rows []xray.ClientTraffic
|
||||||
|
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
|
||||||
|
if err != nil && err != gorm.ErrRecordNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result := make(map[string]int64, len(rows))
|
||||||
|
for _, r := range rows {
|
||||||
|
result[r.Email] = r.LastOnline
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
|
|
||||||
@@ -2220,3 +2247,95 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
|
|||||||
|
|
||||||
return validEmails, extraEmails, nil
|
return validEmails, extraEmails, nil
|
||||||
}
|
}
|
||||||
|
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
||||||
|
oldInbound, err := s.GetInbound(inboundId)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Load Old Data Error")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings map[string]any
|
||||||
|
if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
interfaceClients, ok := settings["clients"].([]any)
|
||||||
|
if !ok {
|
||||||
|
return false, common.NewError("invalid clients format in inbound settings")
|
||||||
|
}
|
||||||
|
|
||||||
|
var newClients []any
|
||||||
|
needApiDel := false
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for _, client := range interfaceClients {
|
||||||
|
c, ok := client.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if cEmail, ok := c["email"].(string); ok && cEmail == email {
|
||||||
|
// matched client, drop it
|
||||||
|
found = true
|
||||||
|
needApiDel, _ = c["enable"].(bool)
|
||||||
|
} else {
|
||||||
|
newClients = append(newClients, client)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return false, common.NewError(fmt.Sprintf("client with email %s not found", email))
|
||||||
|
}
|
||||||
|
if len(newClients) == 0 {
|
||||||
|
return false, common.NewError("no client remained in Inbound")
|
||||||
|
}
|
||||||
|
|
||||||
|
settings["clients"] = newClients
|
||||||
|
newSettings, err := json.MarshalIndent(settings, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbound.Settings = string(newSettings)
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
// remove IP bindings
|
||||||
|
if err := s.DelClientIPs(db, email); err != nil {
|
||||||
|
logger.Error("Error in delete client IPs")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
needRestart := false
|
||||||
|
|
||||||
|
// remove stats too
|
||||||
|
if len(email) > 0 {
|
||||||
|
traffic, err := s.GetClientTrafficByEmail(email)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if traffic != nil {
|
||||||
|
if err := s.DelClientStat(db, email); err != nil {
|
||||||
|
logger.Error("Delete stats Data Error")
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if needApiDel {
|
||||||
|
s.xrayApi.Init(p.GetAPIPort())
|
||||||
|
if err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email); err1 == nil {
|
||||||
|
logger.Debug("Client deleted by api:", email)
|
||||||
|
needRestart = false
|
||||||
|
} else {
|
||||||
|
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) {
|
||||||
|
logger.Debug("User is already deleted. Nothing to do more...")
|
||||||
|
} else {
|
||||||
|
logger.Debug("Error in deleting client by api:", err1)
|
||||||
|
needRestart = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.xrayApi.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return needRestart, db.Save(oldInbound).Error
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -24,6 +25,7 @@ import (
|
|||||||
"x-ui/util/sys"
|
"x-ui/util/sys"
|
||||||
"x-ui/xray"
|
"x-ui/xray"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
"github.com/shirou/gopsutil/v4/host"
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
@@ -343,7 +345,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if major > 25 || (major == 25 && minor > 8) || (major == 25 && minor == 8 && patch >= 3) {
|
if major > 25 || (major == 25 && minor > 9) || (major == 25 && minor == 9 && patch >= 11) {
|
||||||
versions = append(versions, release.TagName)
|
versions = append(versions, release.TagName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -375,6 +377,8 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
|||||||
switch osName {
|
switch osName {
|
||||||
case "darwin":
|
case "darwin":
|
||||||
osName = "macos"
|
osName = "macos"
|
||||||
|
case "windows":
|
||||||
|
osName = "windows"
|
||||||
}
|
}
|
||||||
|
|
||||||
switch arch {
|
switch arch {
|
||||||
@@ -418,19 +422,23 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerService) UpdateXray(version string) error {
|
func (s *ServerService) UpdateXray(version string) error {
|
||||||
|
// 1. Stop xray before doing anything
|
||||||
|
if err := s.StopXrayService(); err != nil {
|
||||||
|
logger.Warning("failed to stop xray before update:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Download the zip
|
||||||
zipFileName, err := s.downloadXRay(version)
|
zipFileName, err := s.downloadXRay(version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer os.Remove(zipFileName)
|
||||||
|
|
||||||
zipFile, err := os.Open(zipFileName)
|
zipFile, err := os.Open(zipFileName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer func() {
|
defer zipFile.Close()
|
||||||
zipFile.Close()
|
|
||||||
os.Remove(zipFileName)
|
|
||||||
}()
|
|
||||||
|
|
||||||
stat, err := zipFile.Stat()
|
stat, err := zipFile.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -441,19 +449,14 @@ func (s *ServerService) UpdateXray(version string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.xrayService.StopXray()
|
// 3. Helper to extract files
|
||||||
defer func() {
|
|
||||||
err := s.xrayService.RestartXray(true)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("start xray failed:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
copyZipFile := func(zipName string, fileName string) error {
|
copyZipFile := func(zipName string, fileName string) error {
|
||||||
zipFile, err := reader.Open(zipName)
|
zipFile, err := reader.Open(zipName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer zipFile.Close()
|
||||||
|
os.MkdirAll(filepath.Dir(fileName), 0755)
|
||||||
os.Remove(fileName)
|
os.Remove(fileName)
|
||||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
|
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -464,11 +467,23 @@ func (s *ServerService) UpdateXray(version string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = copyZipFile("xray", xray.GetBinaryPath())
|
// 4. Extract correct binary
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
targetBinary := filepath.Join("bin", "xray-windows-amd64.exe")
|
||||||
|
err = copyZipFile("xray.exe", targetBinary)
|
||||||
|
} else {
|
||||||
|
err = copyZipFile("xray", xray.GetBinaryPath())
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Restart xray
|
||||||
|
if err := s.xrayService.RestartXray(true); err != nil {
|
||||||
|
logger.Error("start xray failed:", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,3 +886,80 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
|
|||||||
"echConfigList": configList,
|
"echConfigList": configList,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetNewVlessEnc() (any, error) {
|
||||||
|
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(out.String(), "\n")
|
||||||
|
var auths []map[string]string
|
||||||
|
var current map[string]string
|
||||||
|
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(line, "Authentication:") {
|
||||||
|
if current != nil {
|
||||||
|
auths = append(auths, current)
|
||||||
|
}
|
||||||
|
current = map[string]string{
|
||||||
|
"label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")),
|
||||||
|
}
|
||||||
|
} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
|
||||||
|
parts := strings.SplitN(line, ":", 2)
|
||||||
|
if len(parts) == 2 && current != nil {
|
||||||
|
key := strings.Trim(parts[0], `" `)
|
||||||
|
val := strings.Trim(parts[1], `" `)
|
||||||
|
current[key] = val
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current != nil {
|
||||||
|
auths = append(auths, current)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]any{
|
||||||
|
"auths": auths,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetNewUUID() (map[string]string, error) {
|
||||||
|
newUUID, err := uuid.NewRandom()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate UUID: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return map[string]string{
|
||||||
|
"uuid": newUUID.String(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetNewmlkem768() (any, error) {
|
||||||
|
// Run the command
|
||||||
|
cmd := exec.Command(xray.GetBinaryPath(), "mlkem768")
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.Split(out.String(), "\n")
|
||||||
|
|
||||||
|
SeedLine := strings.Split(lines[0], ":")
|
||||||
|
ClientLine := strings.Split(lines[1], ":")
|
||||||
|
|
||||||
|
seed := strings.TrimSpace(SeedLine[1])
|
||||||
|
client := strings.TrimSpace(ClientLine[1])
|
||||||
|
|
||||||
|
keyPair := map[string]any{
|
||||||
|
"seed": seed,
|
||||||
|
"client": client,
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyPair, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"math/big"
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -29,6 +31,7 @@ import (
|
|||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
th "github.com/mymmrac/telego/telegohandler"
|
th "github.com/mymmrac/telego/telegohandler"
|
||||||
tu "github.com/mymmrac/telego/telegoutil"
|
tu "github.com/mymmrac/telego/telegoutil"
|
||||||
|
"github.com/skip2/go-qrcode"
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
"github.com/valyala/fasthttp/fasthttpproxy"
|
"github.com/valyala/fasthttp/fasthttpproxy"
|
||||||
)
|
)
|
||||||
@@ -1355,6 +1358,73 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
case "client_commands":
|
case "client_commands":
|
||||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands"))
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands"))
|
||||||
|
case "client_sub_links":
|
||||||
|
// show user's own clients to choose one for sub links
|
||||||
|
tgUserID := callbackQuery.From.ID
|
||||||
|
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||||
|
if err != nil {
|
||||||
|
// fallback to message
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buttons []telego.InlineKeyboardButton
|
||||||
|
for _, tr := range traffics {
|
||||||
|
buttons = append(buttons, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_sub_links "+tr.Email)))
|
||||||
|
}
|
||||||
|
cols := 1
|
||||||
|
if len(buttons) >= 6 {
|
||||||
|
cols = 2
|
||||||
|
}
|
||||||
|
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard)
|
||||||
|
case "client_individual_links":
|
||||||
|
// show user's clients to choose for individual links
|
||||||
|
tgUserID := callbackQuery.From.ID
|
||||||
|
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buttons2 []telego.InlineKeyboardButton
|
||||||
|
for _, tr := range traffics {
|
||||||
|
buttons2 = append(buttons2, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_individual_links "+tr.Email)))
|
||||||
|
}
|
||||||
|
cols2 := 1
|
||||||
|
if len(buttons2) >= 6 {
|
||||||
|
cols2 = 2
|
||||||
|
}
|
||||||
|
keyboard2 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols2, buttons2...))
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard2)
|
||||||
|
case "client_qr_links":
|
||||||
|
// show user's clients to choose for QR codes
|
||||||
|
tgUserID := callbackQuery.From.ID
|
||||||
|
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOccurred")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(traffics) == 0 {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var buttons3 []telego.InlineKeyboardButton
|
||||||
|
for _, tr := range traffics {
|
||||||
|
buttons3 = append(buttons3, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_qr_links "+tr.Email)))
|
||||||
|
}
|
||||||
|
cols3 := 1
|
||||||
|
if len(buttons3) >= 6 {
|
||||||
|
cols3 = 2
|
||||||
|
}
|
||||||
|
keyboard3 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols3, buttons3...))
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard3)
|
||||||
case "onlines":
|
case "onlines":
|
||||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines"))
|
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines"))
|
||||||
t.onlineClients(chatId)
|
t.onlineClients(chatId)
|
||||||
@@ -1439,6 +1509,23 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
)
|
)
|
||||||
prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment)
|
prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment)
|
||||||
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
||||||
|
default:
|
||||||
|
// dynamic callbacks
|
||||||
|
if strings.HasPrefix(callbackQuery.Data, "client_sub_links ") {
|
||||||
|
email := strings.TrimPrefix(callbackQuery.Data, "client_sub_links ")
|
||||||
|
t.sendClientSubLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(callbackQuery.Data, "client_individual_links ") {
|
||||||
|
email := strings.TrimPrefix(callbackQuery.Data, "client_individual_links ")
|
||||||
|
t.sendClientIndividualLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(callbackQuery.Data, "client_qr_links ") {
|
||||||
|
email := strings.TrimPrefix(callbackQuery.Data, "client_qr_links ")
|
||||||
|
t.sendClientQRLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
case "add_client_ch_default_traffic":
|
case "add_client_ch_default_traffic":
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
@@ -1847,6 +1934,13 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
|||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")),
|
||||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")),
|
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")),
|
||||||
),
|
),
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("pages.settings.subSettings")).WithCallbackData(t.encodeQuery("client_sub_links")),
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links")),
|
||||||
|
),
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("client_qr_links")),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
var ReplyMarkup telego.ReplyMarkup
|
var ReplyMarkup telego.ReplyMarkup
|
||||||
@@ -1908,6 +2002,255 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildSubscriptionURLs builds the HTML sub page URL and JSON subscription URL for a client email
|
||||||
|
func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
||||||
|
// Resolve subId from client email
|
||||||
|
traffic, client, err := t.inboundService.GetClientByEmail(email)
|
||||||
|
_ = traffic
|
||||||
|
if err != nil || client == nil {
|
||||||
|
return "", "", errors.New("client not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Gather settings to construct absolute URLs
|
||||||
|
subDomain, _ := t.settingService.GetSubDomain()
|
||||||
|
subPort, _ := t.settingService.GetSubPort()
|
||||||
|
subPath, _ := t.settingService.GetSubPath()
|
||||||
|
subJsonPath, _ := t.settingService.GetSubJsonPath()
|
||||||
|
subKeyFile, _ := t.settingService.GetSubKeyFile()
|
||||||
|
subCertFile, _ := t.settingService.GetSubCertFile()
|
||||||
|
|
||||||
|
tls := (subKeyFile != "" && subCertFile != "")
|
||||||
|
scheme := "http"
|
||||||
|
if tls {
|
||||||
|
scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallbacks
|
||||||
|
if subDomain == "" {
|
||||||
|
// try panel domain, otherwise OS hostname
|
||||||
|
if d, err := t.settingService.GetWebDomain(); err == nil && d != "" {
|
||||||
|
subDomain = d
|
||||||
|
} else if hostname != "" {
|
||||||
|
subDomain = hostname
|
||||||
|
} else {
|
||||||
|
subDomain = "localhost"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
host := subDomain
|
||||||
|
if (subPort == 443 && tls) || (subPort == 80 && !tls) {
|
||||||
|
// standard ports: no port in host
|
||||||
|
} else {
|
||||||
|
host = fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure paths
|
||||||
|
if !strings.HasPrefix(subPath, "/") {
|
||||||
|
subPath = "/" + subPath
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(subPath, "/") {
|
||||||
|
subPath = subPath + "/"
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(subJsonPath, "/") {
|
||||||
|
subJsonPath = "/" + subJsonPath
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(subJsonPath, "/") {
|
||||||
|
subJsonPath = subJsonPath + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
||||||
|
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
||||||
|
return subURL, subJsonURL, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
||||||
|
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := "Subscription URL:\r\n<code>" + subURL + "</code>\r\n\r\n" +
|
||||||
|
"JSON URL:\r\n<code>" + subJsonURL + "</code>"
|
||||||
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
|
tu.InlineKeyboardRow(
|
||||||
|
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links " + email)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user
|
||||||
|
func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
|
||||||
|
// Build the HTML sub page URL; we'll call it with header Accept to get raw content
|
||||||
|
subURL, _, err := t.buildSubscriptionURLs(email)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to fetch raw subscription links. Prefer plain text response.
|
||||||
|
req, err := http.NewRequest("GET", subURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Force plain text to avoid HTML page; controller respects Accept header
|
||||||
|
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
||||||
|
|
||||||
|
// Use default client with reasonable timeout via context
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
bodyBytes, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If service is configured to encode (Base64), decode it
|
||||||
|
encoded, _ := t.settingService.GetSubEncrypt()
|
||||||
|
var content string
|
||||||
|
if encoded {
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))
|
||||||
|
if err != nil {
|
||||||
|
// fallback to raw text
|
||||||
|
content = string(bodyBytes)
|
||||||
|
} else {
|
||||||
|
content = string(decoded)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = string(bodyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize line endings and trim
|
||||||
|
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||||
|
var cleaned []string
|
||||||
|
for _, l := range lines {
|
||||||
|
l = strings.TrimSpace(l)
|
||||||
|
if l != "" {
|
||||||
|
cleaned = append(cleaned, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cleaned) == 0 {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noResult"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send in chunks to respect message length; use monospace formatting
|
||||||
|
const maxPerMessage = 50
|
||||||
|
for i := 0; i < len(cleaned); i += maxPerMessage {
|
||||||
|
j := i + maxPerMessage
|
||||||
|
if j > len(cleaned) {
|
||||||
|
j = len(cleaned)
|
||||||
|
}
|
||||||
|
chunk := cleaned[i:j]
|
||||||
|
msg := t.I18nBot("subscription.individualLinks") + ":\r\n"
|
||||||
|
for _, link := range chunk {
|
||||||
|
// wrap each link in <code>
|
||||||
|
msg += "<code>" + link + "</code>\r\n"
|
||||||
|
}
|
||||||
|
t.SendMsgToTgbot(chatId, msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendClientQRLinks generates QR images for subscription URL, JSON URL, and a few individual links, then sends them
|
||||||
|
func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
||||||
|
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
||||||
|
if err != nil {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to create QR PNG bytes from content
|
||||||
|
createQR := func(content string, size int) ([]byte, error) {
|
||||||
|
if size <= 0 {
|
||||||
|
size = 256
|
||||||
|
}
|
||||||
|
return qrcode.Encode(content, qrcode.Medium, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inform user
|
||||||
|
t.SendMsgToTgbot(chatId, "QRCode"+":")
|
||||||
|
|
||||||
|
// Send sub URL QR (filename: sub.png)
|
||||||
|
if png, err := createQR(subURL, 320); err == nil {
|
||||||
|
document := tu.Document(
|
||||||
|
tu.ID(chatId),
|
||||||
|
tu.FileFromBytes(png, "sub.png"),
|
||||||
|
)
|
||||||
|
_, _ = bot.SendDocument(context.Background(), document)
|
||||||
|
} else {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send JSON URL QR (filename: subjson.png)
|
||||||
|
if png, err := createQR(subJsonURL, 320); err == nil {
|
||||||
|
document := tu.Document(
|
||||||
|
tu.ID(chatId),
|
||||||
|
tu.FileFromBytes(png, "subjson.png"),
|
||||||
|
)
|
||||||
|
_, _ = bot.SendDocument(context.Background(), document)
|
||||||
|
} else {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also generate a few individual links' QRs (first up to 5)
|
||||||
|
subPageURL := subURL
|
||||||
|
req, err := http.NewRequest("GET", subPageURL, nil)
|
||||||
|
if err == nil {
|
||||||
|
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
req = req.WithContext(ctx)
|
||||||
|
if resp, err := http.DefaultClient.Do(req); err == nil {
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
_ = resp.Body.Close()
|
||||||
|
encoded, _ := t.settingService.GetSubEncrypt()
|
||||||
|
var content string
|
||||||
|
if encoded {
|
||||||
|
if dec, err := base64.StdEncoding.DecodeString(string(body)); err == nil {
|
||||||
|
content = string(dec)
|
||||||
|
} else {
|
||||||
|
content = string(body)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
content = string(body)
|
||||||
|
}
|
||||||
|
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||||
|
var cleaned []string
|
||||||
|
for _, l := range lines {
|
||||||
|
l = strings.TrimSpace(l)
|
||||||
|
if l != "" {
|
||||||
|
cleaned = append(cleaned, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(cleaned) > 0 {
|
||||||
|
max := min(len(cleaned), 5)
|
||||||
|
for i := range max {
|
||||||
|
if png, err := createQR(cleaned[i], 320); err == nil {
|
||||||
|
// Use the email as filename for individual link QR
|
||||||
|
filename := email + ".png"
|
||||||
|
document := tu.Document(
|
||||||
|
tu.ID(chatId),
|
||||||
|
tu.FileFromBytes(png, filename),
|
||||||
|
)
|
||||||
|
_, _ = bot.SendDocument(context.Background(), document)
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
|
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
|
||||||
if len(replyMarkup) > 0 {
|
if len(replyMarkup) > 0 {
|
||||||
for _, adminId := range adminIds {
|
for _, adminId := range adminIds {
|
||||||
@@ -2129,8 +2472,8 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
excludedProtocols := map[model.Protocol]bool{
|
excludedProtocols := map[model.Protocol]bool{
|
||||||
model.DOKODEMO: true,
|
model.Tunnel: true,
|
||||||
model.Socks: true,
|
model.Mixed: true,
|
||||||
model.WireGuard: true,
|
model.WireGuard: true,
|
||||||
model.HTTP: true,
|
model.HTTP: true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package session
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"x-ui/database/model"
|
"x-ui/database/model"
|
||||||
|
|
||||||
@@ -32,6 +33,7 @@ func SetMaxAge(c *gin.Context, maxAge int) {
|
|||||||
Path: defaultPath,
|
Path: defaultPath,
|
||||||
MaxAge: maxAge,
|
MaxAge: maxAge,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,5 +63,6 @@ func ClearSession(c *gin.Context) {
|
|||||||
Path: defaultPath,
|
Path: defaultPath,
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "فشل"
|
"fail" = "فشل"
|
||||||
"comment" = "تعليق"
|
"comment" = "تعليق"
|
||||||
"success" = "تم بنجاح"
|
"success" = "تم بنجاح"
|
||||||
|
"lastOnline" = "آخر متصل"
|
||||||
"getVersion" = "جيب النسخة"
|
"getVersion" = "جيب النسخة"
|
||||||
"install" = "تثبيت"
|
"install" = "تثبيت"
|
||||||
"clients" = "عملاء"
|
"clients" = "عملاء"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "مفيش بروكسي عكسي مضاف."
|
"emptyReverseDesc" = "مفيش بروكسي عكسي مضاف."
|
||||||
"somethingWentWrong" = "حدث خطأ ما"
|
"somethingWentWrong" = "حدث خطأ ما"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "معلومات الاشتراك"
|
||||||
|
"subId" = "معرّف الاشتراك"
|
||||||
|
"status" = "الحالة"
|
||||||
|
"downloaded" = "التنزيل"
|
||||||
|
"uploaded" = "الرفع"
|
||||||
|
"expiry" = "تاريخ الانتهاء"
|
||||||
|
"totalQuota" = "الحصة الإجمالية"
|
||||||
|
"individualLinks" = "روابط فردية"
|
||||||
|
"active" = "نشط"
|
||||||
|
"inactive" = "غير نشط"
|
||||||
|
"unlimited" = "غير محدود"
|
||||||
|
"noExpiry" = "بدون انتهاء"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "الثيم"
|
"theme" = "الثيم"
|
||||||
"dark" = "داكن"
|
"dark" = "داكن"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "خطأ في الحصول على حركات المرور"
|
"trafficGetError" = "خطأ في الحصول على حركات المرور"
|
||||||
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
|
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
|
||||||
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
|
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
|
||||||
|
"getNewVlessEncError" = "حدث خطأ أثناء الحصول على VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "طلب"
|
"request" = "طلب"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Failed"
|
"fail" = "Failed"
|
||||||
"comment" = "Comment"
|
"comment" = "Comment"
|
||||||
"success" = "Successfully"
|
"success" = "Successfully"
|
||||||
|
"lastOnline" = "Last Online"
|
||||||
"getVersion" = "Get Version"
|
"getVersion" = "Get Version"
|
||||||
"install" = "Install"
|
"install" = "Install"
|
||||||
"clients" = "Clients"
|
"clients" = "Clients"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "No added reverse proxies."
|
"emptyReverseDesc" = "No added reverse proxies."
|
||||||
"somethingWentWrong" = "Something went wrong"
|
"somethingWentWrong" = "Something went wrong"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Subscription info"
|
||||||
|
"subId" = "Subscription ID"
|
||||||
|
"status" = "Status"
|
||||||
|
"downloaded" = "Downloaded"
|
||||||
|
"uploaded" = "Uploaded"
|
||||||
|
"expiry" = "Expiry"
|
||||||
|
"totalQuota" = "Total quota"
|
||||||
|
"individualLinks" = "Individual links"
|
||||||
|
"active" = "Active"
|
||||||
|
"inactive" = "Inactive"
|
||||||
|
"unlimited" = "Unlimited"
|
||||||
|
"noExpiry" = "No expiry"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Theme"
|
"theme" = "Theme"
|
||||||
"dark" = "Dark"
|
"dark" = "Dark"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "Error getting traffics."
|
"trafficGetError" = "Error getting traffics."
|
||||||
"getNewX25519CertError" = "Error while obtaining the X25519 certificate."
|
"getNewX25519CertError" = "Error while obtaining the X25519 certificate."
|
||||||
"getNewmldsa65Error" = "Error while obtaining mldsa65."
|
"getNewmldsa65Error" = "Error while obtaining mldsa65."
|
||||||
|
"getNewVlessEncError" = "Error while obtaining VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "Request"
|
"request" = "Request"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Falló"
|
"fail" = "Falló"
|
||||||
"comment" = "Comentario"
|
"comment" = "Comentario"
|
||||||
"success" = "Éxito"
|
"success" = "Éxito"
|
||||||
|
"lastOnline" = "Última conexión"
|
||||||
"getVersion" = "Obtener versión"
|
"getVersion" = "Obtener versión"
|
||||||
"install" = "Instalar"
|
"install" = "Instalar"
|
||||||
"clients" = "Clientes"
|
"clients" = "Clientes"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "No hay proxies inversos añadidos."
|
"emptyReverseDesc" = "No hay proxies inversos añadidos."
|
||||||
"somethingWentWrong" = "Algo salió mal"
|
"somethingWentWrong" = "Algo salió mal"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Información de suscripción"
|
||||||
|
"subId" = "ID de suscripción"
|
||||||
|
"status" = "Estado"
|
||||||
|
"downloaded" = "Descargado"
|
||||||
|
"uploaded" = "Subido"
|
||||||
|
"expiry" = "Caducidad"
|
||||||
|
"totalQuota" = "Cuota total"
|
||||||
|
"individualLinks" = "Enlaces individuales"
|
||||||
|
"active" = "Activo"
|
||||||
|
"inactive" = "Inactivo"
|
||||||
|
"unlimited" = "Ilimitado"
|
||||||
|
"noExpiry" = "Sin caducidad"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Tema"
|
"theme" = "Tema"
|
||||||
"dark" = "Oscuro"
|
"dark" = "Oscuro"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "Error al obtener los tráficos"
|
"trafficGetError" = "Error al obtener los tráficos"
|
||||||
"getNewX25519CertError" = "Error al obtener el certificado X25519."
|
"getNewX25519CertError" = "Error al obtener el certificado X25519."
|
||||||
"getNewmldsa65Error" = "Error al obtener el certificado mldsa65."
|
"getNewmldsa65Error" = "Error al obtener el certificado mldsa65."
|
||||||
|
"getNewVlessEncError" = "Error al obtener el certificado VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "Pedido"
|
"request" = "Pedido"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "ناموفق"
|
"fail" = "ناموفق"
|
||||||
"comment" = "توضیحات"
|
"comment" = "توضیحات"
|
||||||
"success" = "موفق"
|
"success" = "موفق"
|
||||||
|
"lastOnline" = "آخرین فعالیت"
|
||||||
"getVersion" = "دریافت نسخه"
|
"getVersion" = "دریافت نسخه"
|
||||||
"install" = "نصب"
|
"install" = "نصب"
|
||||||
"clients" = "کاربران"
|
"clients" = "کاربران"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است."
|
"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است."
|
||||||
"somethingWentWrong" = "مشکلی پیش آمد"
|
"somethingWentWrong" = "مشکلی پیش آمد"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "اطلاعات سابسکریپشن"
|
||||||
|
"subId" = "شناسه اشتراک"
|
||||||
|
"status" = "وضعیت"
|
||||||
|
"downloaded" = "دانلود"
|
||||||
|
"uploaded" = "آپلود"
|
||||||
|
"expiry" = "تاریخ پایان"
|
||||||
|
"totalQuota" = "حجم کلی"
|
||||||
|
"individualLinks" = "لینکهای تکی"
|
||||||
|
"active" = "فعال"
|
||||||
|
"inactive" = "غیرفعال"
|
||||||
|
"unlimited" = "نامحدود"
|
||||||
|
"noExpiry" = "بدون انقضا"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "تم"
|
"theme" = "تم"
|
||||||
"dark" = "تیره"
|
"dark" = "تیره"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "خطا در دریافت ترافیکها"
|
"trafficGetError" = "خطا در دریافت ترافیکها"
|
||||||
"getNewX25519CertError" = "خطا در دریافت گواهی X25519."
|
"getNewX25519CertError" = "خطا در دریافت گواهی X25519."
|
||||||
"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65."
|
"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65."
|
||||||
|
"getNewVlessEncError" = "خطا در دریافت گواهی VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "درخواست"
|
"request" = "درخواست"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Gagal"
|
"fail" = "Gagal"
|
||||||
"comment" = "Komentar"
|
"comment" = "Komentar"
|
||||||
"success" = "Berhasil"
|
"success" = "Berhasil"
|
||||||
|
"lastOnline" = "Terakhir online"
|
||||||
"getVersion" = "Dapatkan Versi"
|
"getVersion" = "Dapatkan Versi"
|
||||||
"install" = "Instal"
|
"install" = "Instal"
|
||||||
"clients" = "Klien"
|
"clients" = "Klien"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan."
|
"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan."
|
||||||
"somethingWentWrong" = "Terjadi kesalahan"
|
"somethingWentWrong" = "Terjadi kesalahan"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Info langganan"
|
||||||
|
"subId" = "ID langganan"
|
||||||
|
"status" = "Status"
|
||||||
|
"downloaded" = "Diunduh"
|
||||||
|
"uploaded" = "Diunggah"
|
||||||
|
"expiry" = "Kedaluwarsa"
|
||||||
|
"totalQuota" = "Kuota total"
|
||||||
|
"individualLinks" = "Tautan individual"
|
||||||
|
"active" = "Aktif"
|
||||||
|
"inactive" = "Nonaktif"
|
||||||
|
"unlimited" = "Tanpa batas"
|
||||||
|
"noExpiry" = "Tanpa kedaluwarsa"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Tema"
|
"theme" = "Tema"
|
||||||
"dark" = "Gelap"
|
"dark" = "Gelap"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "Gagal mendapatkan data lalu lintas"
|
"trafficGetError" = "Gagal mendapatkan data lalu lintas"
|
||||||
"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519."
|
"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519."
|
||||||
"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65."
|
"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65."
|
||||||
|
"getNewVlessEncError" = "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "Permintaan"
|
"request" = "Permintaan"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "失敗"
|
"fail" = "失敗"
|
||||||
"comment" = "コメント"
|
"comment" = "コメント"
|
||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
|
"lastOnline" = "最終オンライン"
|
||||||
"getVersion" = "バージョン取得"
|
"getVersion" = "バージョン取得"
|
||||||
"install" = "インストール"
|
"install" = "インストール"
|
||||||
"clients" = "クライアント"
|
"clients" = "クライアント"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "追加されたリバースプロキシはありません。"
|
"emptyReverseDesc" = "追加されたリバースプロキシはありません。"
|
||||||
"somethingWentWrong" = "エラーが発生しました"
|
"somethingWentWrong" = "エラーが発生しました"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "サブスクリプション情報"
|
||||||
|
"subId" = "サブスクリプションID"
|
||||||
|
"status" = "ステータス"
|
||||||
|
"downloaded" = "ダウンロード"
|
||||||
|
"uploaded" = "アップロード"
|
||||||
|
"expiry" = "有効期限"
|
||||||
|
"totalQuota" = "合計クォータ"
|
||||||
|
"individualLinks" = "個別リンク"
|
||||||
|
"active" = "有効"
|
||||||
|
"inactive" = "無効"
|
||||||
|
"unlimited" = "無制限"
|
||||||
|
"noExpiry" = "期限なし"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "テーマ"
|
"theme" = "テーマ"
|
||||||
"dark" = "ダーク"
|
"dark" = "ダーク"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "トラフィックの取得中にエラーが発生しました"
|
"trafficGetError" = "トラフィックの取得中にエラーが発生しました"
|
||||||
"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。"
|
"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。"
|
||||||
"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。"
|
"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。"
|
||||||
|
"getNewVlessEncError" = "VlessEnc証明書の取得中にエラーが発生しました。"
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "リクエスト"
|
"request" = "リクエスト"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Falhou"
|
"fail" = "Falhou"
|
||||||
"comment" = "Comentário"
|
"comment" = "Comentário"
|
||||||
"success" = "Com Sucesso"
|
"success" = "Com Sucesso"
|
||||||
|
"lastOnline" = "Última vez online"
|
||||||
"getVersion" = "Obter Versão"
|
"getVersion" = "Obter Versão"
|
||||||
"install" = "Instalar"
|
"install" = "Instalar"
|
||||||
"clients" = "Clientes"
|
"clients" = "Clientes"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "Nenhum proxy reverso adicionado."
|
"emptyReverseDesc" = "Nenhum proxy reverso adicionado."
|
||||||
"somethingWentWrong" = "Algo deu errado"
|
"somethingWentWrong" = "Algo deu errado"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Informações da assinatura"
|
||||||
|
"subId" = "ID da assinatura"
|
||||||
|
"status" = "Status"
|
||||||
|
"downloaded" = "Baixado"
|
||||||
|
"uploaded" = "Enviado"
|
||||||
|
"expiry" = "Validade"
|
||||||
|
"totalQuota" = "Cota total"
|
||||||
|
"individualLinks" = "Links individuais"
|
||||||
|
"active" = "Ativo"
|
||||||
|
"inactive" = "Inativo"
|
||||||
|
"unlimited" = "Ilimitado"
|
||||||
|
"noExpiry" = "Sem validade"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Tema"
|
"theme" = "Tema"
|
||||||
"dark" = "Escuro"
|
"dark" = "Escuro"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "Erro ao obter tráfegos"
|
"trafficGetError" = "Erro ao obter tráfegos"
|
||||||
"getNewX25519CertError" = "Erro ao obter o certificado X25519."
|
"getNewX25519CertError" = "Erro ao obter o certificado X25519."
|
||||||
"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65."
|
"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65."
|
||||||
|
"getNewVlessEncError" = "Erro ao obter o certificado VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "Requisição"
|
"request" = "Requisição"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Ошибка"
|
"fail" = "Ошибка"
|
||||||
"comment" = "Комментарий"
|
"comment" = "Комментарий"
|
||||||
"success" = "Успешно"
|
"success" = "Успешно"
|
||||||
|
"lastOnline" = "Был(а) в сети"
|
||||||
"getVersion" = "Узнать версию"
|
"getVersion" = "Узнать версию"
|
||||||
"install" = "Установка"
|
"install" = "Установка"
|
||||||
"clients" = "Клиенты"
|
"clients" = "Клиенты"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "Нет добавленных реверс-прокси."
|
"emptyReverseDesc" = "Нет добавленных реверс-прокси."
|
||||||
"somethingWentWrong" = "Что-то пошло не так"
|
"somethingWentWrong" = "Что-то пошло не так"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Информация о подписке"
|
||||||
|
"subId" = "ID подписки"
|
||||||
|
"status" = "Статус"
|
||||||
|
"downloaded" = "Загружено"
|
||||||
|
"uploaded" = "Отправлено"
|
||||||
|
"expiry" = "Срок действия"
|
||||||
|
"totalQuota" = "Общий лимит"
|
||||||
|
"individualLinks" = "Индивидуальные ссылки"
|
||||||
|
"active" = "Активна"
|
||||||
|
"inactive" = "Неактивна"
|
||||||
|
"unlimited" = "Безлимит"
|
||||||
|
"noExpiry" = "Без срока"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Тема"
|
"theme" = "Тема"
|
||||||
"dark" = "Темная"
|
"dark" = "Темная"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "Ошибка получения данных о трафике"
|
"trafficGetError" = "Ошибка получения данных о трафике"
|
||||||
"getNewX25519CertError" = "Ошибка при получении сертификата X25519."
|
"getNewX25519CertError" = "Ошибка при получении сертификата X25519."
|
||||||
"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65."
|
"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65."
|
||||||
|
"getNewVlessEncError" = "Ошибка при получении сертификата VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "Запрос"
|
"request" = "Запрос"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Başarısız"
|
"fail" = "Başarısız"
|
||||||
"comment" = "Yorum"
|
"comment" = "Yorum"
|
||||||
"success" = "Başarılı"
|
"success" = "Başarılı"
|
||||||
|
"lastOnline" = "Son çevrimiçi"
|
||||||
"getVersion" = "Sürümü Al"
|
"getVersion" = "Sürümü Al"
|
||||||
"install" = "Yükle"
|
"install" = "Yükle"
|
||||||
"clients" = "Müşteriler"
|
"clients" = "Müşteriler"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "Eklenmiş ters proxy yok."
|
"emptyReverseDesc" = "Eklenmiş ters proxy yok."
|
||||||
"somethingWentWrong" = "Bir şeyler yanlış gitti"
|
"somethingWentWrong" = "Bir şeyler yanlış gitti"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Abonelik Bilgisi"
|
||||||
|
"subId" = "Abonelik Kimliği"
|
||||||
|
"status" = "Durum"
|
||||||
|
"downloaded" = "İndirilen"
|
||||||
|
"uploaded" = "Yüklenen"
|
||||||
|
"expiry" = "Son Kullanma"
|
||||||
|
"totalQuota" = "Toplam Kota"
|
||||||
|
"individualLinks" = "Bireysel Bağlantılar"
|
||||||
|
"active" = "Aktif"
|
||||||
|
"inactive" = "Pasif"
|
||||||
|
"unlimited" = "Sınırsız"
|
||||||
|
"noExpiry" = "Süresiz"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Tema"
|
"theme" = "Tema"
|
||||||
"dark" = "Koyu"
|
"dark" = "Koyu"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "Trafik bilgisi alınırken hata oluştu"
|
"trafficGetError" = "Trafik bilgisi alınırken hata oluştu"
|
||||||
"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu."
|
"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu."
|
||||||
"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu."
|
"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu."
|
||||||
|
"getNewVlessEncError" = "VlessEnc sertifikası alınırken hata oluştu."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "İstek"
|
"request" = "İstek"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Помилка"
|
"fail" = "Помилка"
|
||||||
"comment" = "Коментар"
|
"comment" = "Коментар"
|
||||||
"success" = "Успішно"
|
"success" = "Успішно"
|
||||||
|
"lastOnline" = "Був(ла) онлайн"
|
||||||
"getVersion" = "Отримати версію"
|
"getVersion" = "Отримати версію"
|
||||||
"install" = "Встановити"
|
"install" = "Встановити"
|
||||||
"clients" = "Клієнти"
|
"clients" = "Клієнти"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "Немає доданих зворотних проксі."
|
"emptyReverseDesc" = "Немає доданих зворотних проксі."
|
||||||
"somethingWentWrong" = "Щось пішло не так"
|
"somethingWentWrong" = "Щось пішло не так"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Інформація про підписку"
|
||||||
|
"subId" = "ID підписки"
|
||||||
|
"status" = "Статус"
|
||||||
|
"downloaded" = "Завантажено"
|
||||||
|
"uploaded" = "Відвантажено"
|
||||||
|
"expiry" = "Термін дії"
|
||||||
|
"totalQuota" = "Загальна квота"
|
||||||
|
"individualLinks" = "Окремі посилання"
|
||||||
|
"active" = "Активна"
|
||||||
|
"inactive" = "Неактивна"
|
||||||
|
"unlimited" = "Безліміт"
|
||||||
|
"noExpiry" = "Без строку"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Тема"
|
"theme" = "Тема"
|
||||||
"dark" = "Темна"
|
"dark" = "Темна"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "Помилка отримання даних про трафік"
|
"trafficGetError" = "Помилка отримання даних про трафік"
|
||||||
"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519."
|
"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519."
|
||||||
"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65."
|
"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65."
|
||||||
|
"getNewVlessEncError" = "Помилка при отриманні сертифіката VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "Запит"
|
"request" = "Запит"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "Thất bại"
|
"fail" = "Thất bại"
|
||||||
"comment" = "Bình luận"
|
"comment" = "Bình luận"
|
||||||
"success" = "Thành công"
|
"success" = "Thành công"
|
||||||
|
"lastOnline" = "Lần online gần nhất"
|
||||||
"getVersion" = "Lấy phiên bản"
|
"getVersion" = "Lấy phiên bản"
|
||||||
"install" = "Cài đặt"
|
"install" = "Cài đặt"
|
||||||
"clients" = "Các khách hàng"
|
"clients" = "Các khách hàng"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "Không có proxy ngược nào được thêm."
|
"emptyReverseDesc" = "Không có proxy ngược nào được thêm."
|
||||||
"somethingWentWrong" = "Đã xảy ra lỗi"
|
"somethingWentWrong" = "Đã xảy ra lỗi"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "Thông tin đăng ký"
|
||||||
|
"subId" = "ID đăng ký"
|
||||||
|
"status" = "Trạng thái"
|
||||||
|
"downloaded" = "Đã tải xuống"
|
||||||
|
"uploaded" = "Đã tải lên"
|
||||||
|
"expiry" = "Hết hạn"
|
||||||
|
"totalQuota" = "Tổng hạn mức"
|
||||||
|
"individualLinks" = "Liên kết riêng lẻ"
|
||||||
|
"active" = "Hoạt động"
|
||||||
|
"inactive" = "Không hoạt động"
|
||||||
|
"unlimited" = "Không giới hạn"
|
||||||
|
"noExpiry" = "Không hết hạn"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "Chủ đề"
|
"theme" = "Chủ đề"
|
||||||
"dark" = "Tối"
|
"dark" = "Tối"
|
||||||
@@ -265,7 +280,8 @@
|
|||||||
"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng"
|
"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng"
|
||||||
"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng"
|
"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng"
|
||||||
"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519."
|
"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519."
|
||||||
"getNewmldsa65Error" = "Lỗi khi lấy chúng tôi mldsa65."
|
"getNewmldsa65Error" = "Lỗi khi lấy chứng chỉ mldsa65."
|
||||||
|
"getNewVlessEncError" = "Lỗi khi lấy chứng chỉ VlessEnc."
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "Lời yêu cầu"
|
"request" = "Lời yêu cầu"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "失败"
|
"fail" = "失败"
|
||||||
"comment" = "评论"
|
"comment" = "评论"
|
||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
|
"lastOnline" = "上次在线"
|
||||||
"getVersion" = "获取版本"
|
"getVersion" = "获取版本"
|
||||||
"install" = "安装"
|
"install" = "安装"
|
||||||
"clients" = "客户端"
|
"clients" = "客户端"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "未添加反向代理。"
|
"emptyReverseDesc" = "未添加反向代理。"
|
||||||
"somethingWentWrong" = "出了点问题"
|
"somethingWentWrong" = "出了点问题"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "订阅信息"
|
||||||
|
"subId" = "订阅 ID"
|
||||||
|
"status" = "状态"
|
||||||
|
"downloaded" = "已下载"
|
||||||
|
"uploaded" = "已上传"
|
||||||
|
"expiry" = "到期"
|
||||||
|
"totalQuota" = "总配额"
|
||||||
|
"individualLinks" = "单独链接"
|
||||||
|
"active" = "启用"
|
||||||
|
"inactive" = "停用"
|
||||||
|
"unlimited" = "无限制"
|
||||||
|
"noExpiry" = "无到期"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "主题"
|
"theme" = "主题"
|
||||||
"dark" = "暗色"
|
"dark" = "暗色"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "获取流量数据时出错"
|
"trafficGetError" = "获取流量数据时出错"
|
||||||
"getNewX25519CertError" = "获取X25519证书时出错。"
|
"getNewX25519CertError" = "获取X25519证书时出错。"
|
||||||
"getNewmldsa65Error" = "获取mldsa65证书时出错。"
|
"getNewmldsa65Error" = "获取mldsa65证书时出错。"
|
||||||
|
"getNewVlessEncError" = "获取VlessEnc证书时出错。"
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "请求"
|
"request" = "请求"
|
||||||
|
|||||||
@@ -50,6 +50,7 @@
|
|||||||
"fail" = "失敗"
|
"fail" = "失敗"
|
||||||
"comment" = "評論"
|
"comment" = "評論"
|
||||||
"success" = "成功"
|
"success" = "成功"
|
||||||
|
"lastOnline" = "上次上線"
|
||||||
"getVersion" = "獲取版本"
|
"getVersion" = "獲取版本"
|
||||||
"install" = "安裝"
|
"install" = "安裝"
|
||||||
"clients" = "客戶端"
|
"clients" = "客戶端"
|
||||||
@@ -71,6 +72,20 @@
|
|||||||
"emptyReverseDesc" = "未添加反向代理。"
|
"emptyReverseDesc" = "未添加反向代理。"
|
||||||
"somethingWentWrong" = "發生錯誤"
|
"somethingWentWrong" = "發生錯誤"
|
||||||
|
|
||||||
|
[subscription]
|
||||||
|
"title" = "訂閱資訊"
|
||||||
|
"subId" = "訂閱 ID"
|
||||||
|
"status" = "狀態"
|
||||||
|
"downloaded" = "已下載"
|
||||||
|
"uploaded" = "已上傳"
|
||||||
|
"expiry" = "到期"
|
||||||
|
"totalQuota" = "總配額"
|
||||||
|
"individualLinks" = "個別連結"
|
||||||
|
"active" = "啟用"
|
||||||
|
"inactive" = "停用"
|
||||||
|
"unlimited" = "無限制"
|
||||||
|
"noExpiry" = "無到期"
|
||||||
|
|
||||||
[menu]
|
[menu]
|
||||||
"theme" = "主題"
|
"theme" = "主題"
|
||||||
"dark" = "深色"
|
"dark" = "深色"
|
||||||
@@ -266,6 +281,7 @@
|
|||||||
"trafficGetError" = "取得流量資料時發生錯誤"
|
"trafficGetError" = "取得流量資料時發生錯誤"
|
||||||
"getNewX25519CertError" = "取得X25519憑證時發生錯誤。"
|
"getNewX25519CertError" = "取得X25519憑證時發生錯誤。"
|
||||||
"getNewmldsa65Error" = "取得mldsa65憑證時發生錯誤。"
|
"getNewmldsa65Error" = "取得mldsa65憑證時發生錯誤。"
|
||||||
|
"getNewVlessEncError" = "取得VlessEnc憑證時發生錯誤。"
|
||||||
|
|
||||||
[pages.inbounds.stream.general]
|
[pages.inbounds.stream.general]
|
||||||
"request" = "請求"
|
"request" = "請求"
|
||||||
|
|||||||
31
web/web.go
31
web/web.go
@@ -31,7 +31,7 @@ import (
|
|||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed assets/*
|
//go:embed assets
|
||||||
var assetsFS embed.FS
|
var assetsFS embed.FS
|
||||||
|
|
||||||
//go:embed html/*
|
//go:embed html/*
|
||||||
@@ -78,6 +78,15 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time {
|
|||||||
return startTime
|
return startTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Expose embedded resources for reuse by other servers (e.g., sub server)
|
||||||
|
func EmbeddedHTML() embed.FS {
|
||||||
|
return htmlFS
|
||||||
|
}
|
||||||
|
|
||||||
|
func EmbeddedAssets() embed.FS {
|
||||||
|
return assetsFS
|
||||||
|
}
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
@@ -176,10 +185,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/API/"})))
|
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/api/"})))
|
||||||
assetsBasePath := basePath + "assets/"
|
assetsBasePath := basePath + "assets/"
|
||||||
|
|
||||||
store := cookie.NewStore(secret)
|
store := cookie.NewStore(secret)
|
||||||
|
// Configure default session cookie options, including expiration (MaxAge)
|
||||||
|
if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil {
|
||||||
|
store.Options(sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: sessionMaxAge * 60, // minutes -> seconds
|
||||||
|
HttpOnly: true,
|
||||||
|
SameSite: http.SameSiteLaxMode,
|
||||||
|
})
|
||||||
|
}
|
||||||
engine.Use(sessions.Sessions("3x-ui", store))
|
engine.Use(sessions.Sessions("3x-ui", store))
|
||||||
engine.Use(func(c *gin.Context) {
|
engine.Use(func(c *gin.Context) {
|
||||||
c.Set("base_path", basePath)
|
c.Set("base_path", basePath)
|
||||||
@@ -201,7 +219,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
i18nWebFunc := func(key string, params ...string) string {
|
i18nWebFunc := func(key string, params ...string) string {
|
||||||
return locale.I18n(locale.Web, key, params...)
|
return locale.I18n(locale.Web, key, params...)
|
||||||
}
|
}
|
||||||
engine.FuncMap["i18n"] = i18nWebFunc
|
// Register template functions before loading templates
|
||||||
|
funcMap := template.FuncMap{
|
||||||
|
"i18n": i18nWebFunc,
|
||||||
|
}
|
||||||
|
engine.SetFuncMap(funcMap)
|
||||||
engine.Use(locale.LocalizerMiddleware())
|
engine.Use(locale.LocalizerMiddleware())
|
||||||
|
|
||||||
// set static files and template
|
// set static files and template
|
||||||
@@ -211,11 +233,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// Use the registered func map with the loaded templates
|
||||||
engine.LoadHTMLFiles(files...)
|
engine.LoadHTMLFiles(files...)
|
||||||
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
|
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
|
||||||
} else {
|
} else {
|
||||||
// for production
|
// for production
|
||||||
template, err := s.getHtmlTemplate(engine.FuncMap)
|
template, err := s.getHtmlTemplate(funcMap)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
windows_files/SSL/Win64OpenSSL_Light-3_5_2.exe
Normal file
BIN
windows_files/SSL/Win64OpenSSL_Light-3_5_2.exe
Normal file
Binary file not shown.
13
windows_files/readme.txt
Normal file
13
windows_files/readme.txt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
you can't install fail2ban on windows
|
||||||
|
we don't have bash menu for windows
|
||||||
|
if you forgot your password you need to check your database with https://sqlitebrowser.org/
|
||||||
|
the app need to be open all the time
|
||||||
|
|
||||||
|
default setting:
|
||||||
|
http://localhost:2053/
|
||||||
|
user: admin
|
||||||
|
pass: admin
|
||||||
|
port: 2053
|
||||||
|
|
||||||
|
|
||||||
|
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt
|
||||||
@@ -11,4 +11,5 @@ type ClientTraffic struct {
|
|||||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||||
Total int64 `json:"total" form:"total"`
|
Total int64 `json:"total" form:"total"`
|
||||||
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
||||||
|
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package xray
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"x-ui/logger"
|
||||||
@@ -20,6 +21,12 @@ func (lw *LogWriter) Write(m []byte) (n int, err error) {
|
|||||||
|
|
||||||
// Convert the data to a string
|
// Convert the data to a string
|
||||||
message := strings.TrimSpace(string(m))
|
message := strings.TrimSpace(string(m))
|
||||||
|
msgLowerAll := strings.ToLower(message)
|
||||||
|
|
||||||
|
// Suppress noisy Windows process-kill signal that surfaces as exit status 1
|
||||||
|
if runtime.GOOS == "windows" && strings.Contains(msgLowerAll, "exit status 1") {
|
||||||
|
return len(m), nil
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the message contains a crash
|
// Check if the message contains a crash
|
||||||
if crashRegex.MatchString(message) {
|
if crashRegex.MatchString(message) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -224,6 +225,15 @@ func (p *process) Start() (err error) {
|
|||||||
go func() {
|
go func() {
|
||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// On Windows, killing the process results in "exit status 1" which isn't an error for us
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
errStr := strings.ToLower(err.Error())
|
||||||
|
if strings.Contains(errStr, "exit status 1") {
|
||||||
|
// Suppress noisy log on graceful stop
|
||||||
|
p.exitErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
logger.Error("Failure in running xray-core:", err)
|
logger.Error("Failure in running xray-core:", err)
|
||||||
p.exitErr = err
|
p.exitErr = err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user