mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-19 17:15:49 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ffd27c0aa | ||
|
|
054cb1dea0 | ||
|
|
3757ae0b11 | ||
|
|
e3883fca87 | ||
|
|
b46a0b404b | ||
|
|
0ce58a095a | ||
|
|
59ea2645db | ||
|
|
8c8d280f14 | ||
|
|
c720008187 | ||
|
|
170d24499e | ||
|
|
99c79d4056 | ||
|
|
fcdeb1fc79 | ||
|
|
0a58b5e745 | ||
|
|
db7e7dcd29 | ||
|
|
01b8a27996 |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -11,4 +11,4 @@ issuehunt: # Replace with a single IssueHunt username
|
|||||||
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
|
||||||
polar: # Replace with a single Polar username
|
polar: # Replace with a single Polar username
|
||||||
buy_me_a_coffee: mhsanaei
|
buy_me_a_coffee: mhsanaei
|
||||||
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
|
custom: https://nowpayments.io/donation/hsanaei
|
||||||
|
|||||||
35
.vscode/launch.json
vendored
Normal file
35
.vscode/launch.json
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"$schema": "vscode://schemas/launch",
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Run 3x-ui (Debug)",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"XUI_DEBUG": "true"
|
||||||
|
},
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Run 3x-ui (Debug, custom env)",
|
||||||
|
"type": "go",
|
||||||
|
"request": "launch",
|
||||||
|
"mode": "auto",
|
||||||
|
"program": "${workspaceFolder}",
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
// Set to true to serve assets/templates directly from disk for development
|
||||||
|
"XUI_DEBUG": "true",
|
||||||
|
// Uncomment to override DB folder location (by default uses working dir on Windows when debug)
|
||||||
|
// "XUI_DB_FOLDER": "${workspaceFolder}",
|
||||||
|
// Example: override log level (debug|info|notice|warn|error)
|
||||||
|
// "XUI_LOG_LEVEL": "debug"
|
||||||
|
},
|
||||||
|
"console": "integratedTerminal"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
40
.vscode/tasks.json
vendored
Normal file
40
.vscode/tasks.json
vendored
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"version": "2.0.0",
|
||||||
|
"tasks": [
|
||||||
|
{
|
||||||
|
"label": "go: build",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["build", "-o", "bin/3x-ui.exe", "./main.go"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": ["$go"],
|
||||||
|
"group": { "kind": "build", "isDefault": true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: run",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["run", "./main.go"],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}",
|
||||||
|
"env": {
|
||||||
|
"XUI_DEBUG": "true"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"problemMatcher": ["$go"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "go: test",
|
||||||
|
"type": "shell",
|
||||||
|
"command": "go",
|
||||||
|
"args": ["test", "./..."],
|
||||||
|
"options": {
|
||||||
|
"cwd": "${workspaceFolder}"
|
||||||
|
},
|
||||||
|
"problemMatcher": ["$go"],
|
||||||
|
"group": "test"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -41,15 +41,13 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||||||
|
|
||||||
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
|
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
|
||||||
|
|
||||||
<p align="left">
|
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||||
<img src="./media/buymeacoffe.png" alt="Image">
|
</a>
|
||||||
</a>
|
</br>
|
||||||
</p>
|
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
</a>
|
||||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
|
||||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
|
||||||
|
|
||||||
## النجوم عبر الزمن
|
## النجوم عبر الزمن
|
||||||
|
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
|
|||||||
|
|
||||||
**Si este proyecto te es útil, puedes darle una**:star2:
|
**Si este proyecto te es útil, puedes darle una**:star2:
|
||||||
|
|
||||||
<p align="left">
|
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||||
<img src="./media/buymeacoffe.png" alt="Image">
|
</a>
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
</br>
|
||||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||||
|
</a>
|
||||||
|
|
||||||
## Estrellas a lo Largo del Tiempo
|
## Estrellas a lo Largo del Tiempo
|
||||||
|
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||||||
|
|
||||||
**اگر این پروژه برای شما مفید است، میتوانید به آن یک**:star2: بدهید
|
**اگر این پروژه برای شما مفید است، میتوانید به آن یک**:star2: بدهید
|
||||||
|
|
||||||
<p align="left">
|
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||||
<img src="./media/buymeacoffe.png" alt="Image">
|
</a>
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
</br>
|
||||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||||
|
</a>
|
||||||
|
|
||||||
## ستارهها در طول زمان
|
## ستارهها در طول زمان
|
||||||
|
|
||||||
|
|||||||
15
README.md
15
README.md
@@ -41,15 +41,14 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
|
|||||||
|
|
||||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||||
|
|
||||||
<p align="left">
|
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||||
<img src="./media/buymeacoffe.png" alt="Image">
|
</a>
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
</br>
|
||||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||||
|
</a>
|
||||||
|
|
||||||
## Stargazers over Time
|
## Stargazers over Time
|
||||||
|
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||||||
|
|
||||||
**Если этот проект полезен для вас, вы можете поставить ему**:star2:
|
**Если этот проект полезен для вас, вы можете поставить ему**:star2:
|
||||||
|
|
||||||
<p align="left">
|
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||||
<img src="./media/buymeacoffe.png" alt="Image">
|
</a>
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
</br>
|
||||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||||
|
</a>
|
||||||
|
|
||||||
## Звезды с течением времени
|
## Звезды с течением времени
|
||||||
|
|
||||||
|
|||||||
@@ -41,15 +41,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
|||||||
|
|
||||||
**如果这个项目对您有帮助,您可以给它一个**:star2:
|
**如果这个项目对您有帮助,您可以给它一个**:star2:
|
||||||
|
|
||||||
<p align="left">
|
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
|
||||||
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
|
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
|
||||||
<img src="./media/buymeacoffe.png" alt="Image">
|
</a>
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
</br>
|
||||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
<a href="https://nowpayments.io/donation/hsanaei" target="_blank" rel="noreferrer noopener">
|
||||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
<img src="./media/donation-button-black.svg" alt="Crypto donation button by NOWPayments">
|
||||||
|
</a>
|
||||||
|
|
||||||
## 随时间变化的星标数
|
## 随时间变化的星标数
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
2.8.1
|
2.8.2
|
||||||
@@ -9,10 +9,10 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"slices"
|
"slices"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/util/crypto"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
|
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -141,6 +141,9 @@ func InitDB(dbPath string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isUsersEmpty, err := isTableEmpty("users")
|
isUsersEmpty, err := isTableEmpty("users")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if err := initUser(); err != nil {
|
if err := initUser(); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package model
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/util/json_util"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Protocol string
|
type Protocol string
|
||||||
|
|||||||
16
main.go
16
main.go
@@ -9,14 +9,14 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
_ "unsafe"
|
_ "unsafe"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/sub"
|
"github.com/mhsanaei/3x-ui/sub"
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/util/crypto"
|
||||||
"x-ui/web"
|
"github.com/mhsanaei/3x-ui/web"
|
||||||
"x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/web/global"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.1 KiB |
BIN
media/default-yellow.png
Normal file
BIN
media/default-yellow.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.7 KiB |
1
media/donation-button-black.svg
Normal file
1
media/donation-button-black.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 10 KiB |
26
sub/sub.go
26
sub/sub.go
@@ -13,13 +13,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
webpkg "x-ui/web"
|
webpkg "github.com/mhsanaei/3x-ui/web"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/web/locale"
|
||||||
"x-ui/web/middleware"
|
"github.com/mhsanaei/3x-ui/web/middleware"
|
||||||
"x-ui/web/network"
|
"github.com/mhsanaei/3x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -30,7 +30,7 @@ func setEmbeddedTemplates(engine *gin.Engine) error {
|
|||||||
webpkg.EmbeddedHTML(),
|
webpkg.EmbeddedHTML(),
|
||||||
"html/common/page.html",
|
"html/common/page.html",
|
||||||
"html/component/aThemeSwitch.html",
|
"html/component/aThemeSwitch.html",
|
||||||
"html/subscription.html",
|
"html/settings/panel/subscription/subpage.html",
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -85,6 +85,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine if JSON subscription endpoint is enabled
|
||||||
|
subJsonEnable, err := s.settingService.GetSubJsonEnable()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Set base_path based on LinksPath for template rendering
|
// Set base_path based on LinksPath for template rendering
|
||||||
engine.Use(func(c *gin.Context) {
|
engine.Use(func(c *gin.Context) {
|
||||||
c.Set("base_path", LinksPath)
|
c.Set("base_path", LinksPath)
|
||||||
@@ -186,7 +192,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
g := engine.Group("/")
|
g := engine.Group("/")
|
||||||
|
|
||||||
s.sub = NewSUBController(
|
s.sub = NewSUBController(
|
||||||
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
|
||||||
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
@@ -207,7 +213,7 @@ func (s *Server) getHtmlFiles() ([]string, error) {
|
|||||||
files = append(files, theme)
|
files = append(files, theme)
|
||||||
}
|
}
|
||||||
// page itself
|
// page itself
|
||||||
page := filepath.Join(dir, "web", "html", "subscription.html")
|
page := filepath.Join(dir, "web", "html", "subpage.html")
|
||||||
if _, err := os.Stat(page); err == nil {
|
if _, err := os.Stat(page); err == nil {
|
||||||
files = append(files, page)
|
files = append(files, page)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import (
|
|||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"x-ui/config"
|
|
||||||
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -13,6 +14,7 @@ type SUBController struct {
|
|||||||
subTitle string
|
subTitle string
|
||||||
subPath string
|
subPath string
|
||||||
subJsonPath string
|
subJsonPath string
|
||||||
|
jsonEnabled bool
|
||||||
subEncrypt bool
|
subEncrypt bool
|
||||||
updateInterval string
|
updateInterval string
|
||||||
|
|
||||||
@@ -24,6 +26,7 @@ func NewSUBController(
|
|||||||
g *gin.RouterGroup,
|
g *gin.RouterGroup,
|
||||||
subPath string,
|
subPath string,
|
||||||
jsonPath string,
|
jsonPath string,
|
||||||
|
jsonEnabled bool,
|
||||||
encrypt bool,
|
encrypt bool,
|
||||||
showInfo bool,
|
showInfo bool,
|
||||||
rModel string,
|
rModel string,
|
||||||
@@ -39,6 +42,7 @@ func NewSUBController(
|
|||||||
subTitle: subTitle,
|
subTitle: subTitle,
|
||||||
subPath: subPath,
|
subPath: subPath,
|
||||||
subJsonPath: jsonPath,
|
subJsonPath: jsonPath,
|
||||||
|
jsonEnabled: jsonEnabled,
|
||||||
subEncrypt: encrypt,
|
subEncrypt: encrypt,
|
||||||
updateInterval: update,
|
updateInterval: update,
|
||||||
|
|
||||||
@@ -51,10 +55,11 @@ func NewSUBController(
|
|||||||
|
|
||||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||||
gLink := g.Group(a.subPath)
|
gLink := g.Group(a.subPath)
|
||||||
gJson := g.Group(a.subJsonPath)
|
|
||||||
|
|
||||||
gLink.GET(":subid", a.subs)
|
gLink.GET(":subid", a.subs)
|
||||||
gJson.GET(":subid", a.subJsons)
|
if a.jsonEnabled {
|
||||||
|
gJson := g.Group(a.subJsonPath)
|
||||||
|
gJson.GET(":subid", a.subJsons)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *SUBController) subs(c *gin.Context) {
|
func (a *SUBController) subs(c *gin.Context) {
|
||||||
@@ -74,8 +79,11 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||||||
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||||
// Build page data in service
|
// Build page data in service
|
||||||
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
||||||
|
if !a.jsonEnabled {
|
||||||
|
subJsonURL = ""
|
||||||
|
}
|
||||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
|
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
|
||||||
c.HTML(200, "subscription.html", gin.H{
|
c.HTML(200, "subpage.html", gin.H{
|
||||||
"title": "subscription.title",
|
"title": "subscription.title",
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
"host": page.Host,
|
"host": page.Host,
|
||||||
|
|||||||
@@ -6,12 +6,12 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/util/json_util"
|
||||||
"x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/util/random"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed default.json
|
//go:embed default.json
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/goccy/go-json"
|
"github.com/goccy/go-json"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
"x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/util/random"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SubService struct {
|
type SubService struct {
|
||||||
@@ -1007,7 +1007,7 @@ func searchHost(headers any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// PageData is a view model for subscription.html
|
// PageData is a view model for subpage.html
|
||||||
type PageData struct {
|
type PageData struct {
|
||||||
Host string
|
Host string
|
||||||
BasePath string
|
BasePath string
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewErrorf(format string, a ...any) error {
|
func NewErrorf(format string, a ...any) error {
|
||||||
|
|||||||
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
@@ -26,7 +26,8 @@ class AllSetting {
|
|||||||
this.twoFactorEnable = false;
|
this.twoFactorEnable = false;
|
||||||
this.twoFactorToken = "";
|
this.twoFactorToken = "";
|
||||||
this.xrayTemplateConfig = "";
|
this.xrayTemplateConfig = "";
|
||||||
this.subEnable = false;
|
this.subEnable = true;
|
||||||
|
this.subJsonEnable = false;
|
||||||
this.subTitle = "";
|
this.subTitle = "";
|
||||||
this.subListen = "";
|
this.subListen = "";
|
||||||
this.subPort = 2096;
|
this.subPort = 2096;
|
||||||
|
|||||||
@@ -50,7 +50,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function drawQR(value) {
|
function drawQR(value) {
|
||||||
try { new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); } catch (e) { console.warn(e); }
|
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
|
// Try to extract a human label (email/ps) from different link types
|
||||||
@@ -61,22 +65,18 @@
|
|||||||
if (json.ps) return json.ps;
|
if (json.ps) return json.ps;
|
||||||
if (json.add && json.id) return json.add; // fallback host
|
if (json.add && json.id) return json.add; // fallback host
|
||||||
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
|
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
|
||||||
// vless://<id>@host:port?...#name
|
|
||||||
const hashIdx = link.indexOf('#');
|
const hashIdx = link.indexOf('#');
|
||||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||||
// email sometimes in query params like sni or remark
|
|
||||||
const qIdx = link.indexOf('?');
|
const qIdx = link.indexOf('?');
|
||||||
if (qIdx !== -1) {
|
if (qIdx !== -1) {
|
||||||
const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
|
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('remark')) return qs.get('remark');
|
||||||
if (qs.get('email')) return qs.get('email');
|
if (qs.get('email')) return qs.get('email');
|
||||||
}
|
}
|
||||||
// else take user@host
|
|
||||||
const at = link.indexOf('@');
|
const at = link.indexOf('@');
|
||||||
const protSep = link.indexOf('://');
|
const protSep = link.indexOf('://');
|
||||||
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
|
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
|
||||||
} else if (link.startsWith('ss://')) {
|
} else if (link.startsWith('ss://')) {
|
||||||
// shadowsocks: label often after #
|
|
||||||
const hashIdx = link.indexOf('#');
|
const hashIdx = link.indexOf('#');
|
||||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||||
}
|
}
|
||||||
@@ -96,14 +96,16 @@
|
|||||||
},
|
},
|
||||||
async mounted() {
|
async mounted() {
|
||||||
this.lang = LanguageManager.getLanguage();
|
this.lang = LanguageManager.getLanguage();
|
||||||
// Discover subJsonUrl if provided via template bootstrap
|
|
||||||
const tpl = document.getElementById('subscription-data');
|
const tpl = document.getElementById('subscription-data');
|
||||||
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
|
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
|
||||||
if (sj) this.app.subJsonUrl = sj;
|
if (sj) this.app.subJsonUrl = sj;
|
||||||
drawQR(this.app.subUrl);
|
drawQR(this.app.subUrl);
|
||||||
// Draw second QR if available
|
try {
|
||||||
try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ }
|
const elJson = document.getElementById('qrcode-subjson');
|
||||||
// Track viewport width for responsive behavior
|
if (elJson && this.app.subJsonUrl) {
|
||||||
|
new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 });
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
||||||
window.addEventListener('resize', this._onResize);
|
window.addEventListener('resize', this._onResize);
|
||||||
},
|
},
|
||||||
@@ -111,15 +113,45 @@
|
|||||||
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
isMobile() { return this.viewportWidth < 576; },
|
isMobile() {
|
||||||
isUnlimited() { return !this.app.totalByte; },
|
return this.viewportWidth < 576;
|
||||||
|
},
|
||||||
|
isUnlimited() {
|
||||||
|
return !this.app.totalByte;
|
||||||
|
},
|
||||||
isActive() {
|
isActive() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
|
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
|
||||||
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
|
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
|
||||||
return expiryOk && trafficOk;
|
return expiryOk && trafficOk;
|
||||||
},
|
},
|
||||||
|
shadowrocketUrl() {
|
||||||
|
const rawUrl = this.app.subUrl + '?flag=shadowrocket';
|
||||||
|
const base64Url = btoa(rawUrl);
|
||||||
|
const remark = encodeURIComponent(this.app.sId || 'Subscription');
|
||||||
|
return `shadowrocket://add/sub/${base64Url}?remark=${remark}`;
|
||||||
|
},
|
||||||
|
v2boxUrl() {
|
||||||
|
return `v2box://install-sub?url=${encodeURIComponent(this.app.subUrl)}&name=${encodeURIComponent(this.app.sId)}`;
|
||||||
|
},
|
||||||
|
streisandUrl() {
|
||||||
|
return `streisand://import/${encodeURIComponent(this.app.subUrl)}`;
|
||||||
|
},
|
||||||
|
v2raytunUrl() {
|
||||||
|
return this.app.subUrl;
|
||||||
|
},
|
||||||
|
npvtunUrl() {
|
||||||
|
return this.app.subUrl;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
renderLink,
|
||||||
|
copy,
|
||||||
|
open,
|
||||||
|
linkName,
|
||||||
|
i18nLabel(key) {
|
||||||
|
return '{{ i18n "' + key + '" }}';
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: { renderLink, copy, open, linkName, i18nLabel(key) { return '{{ i18n "' + key + '" }}'; } },
|
|
||||||
});
|
});
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package controller
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/web/locale"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,18 +5,18 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/web/session"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginForm struct {
|
type LoginForm struct {
|
||||||
Username string `json:"username" form:"username"`
|
Username string `json:"username" form:"username"`
|
||||||
Password string `json:"password" form:"password"`
|
Password string `json:"password" form:"password"`
|
||||||
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IndexController struct {
|
type IndexController struct {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/web/global"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/util/crypto"
|
||||||
"x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/web/entity"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
"x-ui/web/session"
|
"github.com/mhsanaei/3x-ui/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/web/entity"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Msg struct {
|
type Msg struct {
|
||||||
@@ -42,6 +42,7 @@ type AllSetting struct {
|
|||||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||||
|
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
|
||||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||||
SubListen string `json:"subListen" form:"subListen"`
|
SubListen string `json:"subListen" form:"subListen"`
|
||||||
SubPort int `json:"subPort" form:"subPort"`
|
SubPort int `json:"subPort" form:"subPort"`
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<template slot="content" >
|
<template slot="content" >
|
||||||
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||||
</template>
|
</template>
|
||||||
<template v-if="client.enable && isClientOnline(client.email) && !isClientDepleted">
|
<template v-if="client.enable && isClientOnline(client.email)">
|
||||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -49,9 +49,9 @@
|
|||||||
<a-space direction="horizontal" :size="2">
|
<a-space direction="horizontal" :size="2">
|
||||||
<a-tooltip>
|
<a-tooltip>
|
||||||
<template slot="title">
|
<template slot="title">
|
||||||
<template v-if="isClientDepleted">{{ i18n "depleted" }}</template>
|
<template v-if="isClientDepleted(record, client.email)">{{ i18n "depleted" }}</template>
|
||||||
<template v-if="!isClientDepleted && !client.enable">{{ i18n "disabled" }}</template>
|
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||||
<template v-if="!isClientDepleted && client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||||
</template>
|
</template>
|
||||||
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
|
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
|
||||||
</a-tooltip>
|
</a-tooltip>
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
|
||||||
</td>
|
</td>
|
||||||
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
<td class="tr-table-bar" v-else-if="client.totalGB > 0">
|
||||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||||
</td>
|
</td>
|
||||||
<td v-else class="infinite-bar tr-table-bar">
|
<td v-else class="infinite-bar tr-table-bar">
|
||||||
<a-progress :show-info="false" :percent="100"></a-progress>
|
<a-progress :show-info="false" :percent="100"></a-progress>
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<tr class="tr-table-box">
|
<tr class="tr-table-box">
|
||||||
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||||
<td class="infinite-bar tr-table-bar">
|
<td class="infinite-bar tr-table-bar">
|
||||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||||
</td>
|
</td>
|
||||||
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
<td class="tr-table-lt">[[ client.reset + "d" ]]</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
</template>
|
</template>
|
||||||
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</td>
|
</td>
|
||||||
<td width="120px" v-else class="infinite-bar">
|
<td width="120px" v-else class="infinite-bar">
|
||||||
@@ -247,7 +247,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<a-progress :show-info="false" :status="isClientEnabled(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||||
</a-popover>
|
</a-popover>
|
||||||
</td>
|
</td>
|
||||||
<td width="60px">[[ client.reset + "d" ]]</td>
|
<td width="60px">[[ client.reset + "d" ]]</td>
|
||||||
|
|||||||
@@ -568,8 +568,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
|
<td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag color="blue">[[ i18n("pages.inbounds.periodicTrafficReset." +
|
<a-tag color="blue">[[ dbInbound.trafficReset ]]</a-tag>
|
||||||
dbInbound.trafficReset) ]]</a-tag>
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
@@ -737,10 +736,11 @@
|
|||||||
refreshing: false,
|
refreshing: false,
|
||||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||||
subSettings: {
|
subSettings: {
|
||||||
enable: false,
|
enable: true,
|
||||||
subTitle: '',
|
subTitle: '',
|
||||||
subURI: '',
|
subURI: '',
|
||||||
subJsonURI: '',
|
subJsonURI: '',
|
||||||
|
subJsonEnable: false,
|
||||||
},
|
},
|
||||||
remarkModel: '-ieo',
|
remarkModel: '-ieo',
|
||||||
datepicker: 'gregorian',
|
datepicker: 'gregorian',
|
||||||
@@ -796,7 +796,8 @@
|
|||||||
enable: subEnable,
|
enable: subEnable,
|
||||||
subTitle: subTitle,
|
subTitle: subTitle,
|
||||||
subURI: subURI,
|
subURI: subURI,
|
||||||
subJsonURI: subJsonURI
|
subJsonURI: subJsonURI,
|
||||||
|
subJsonEnable: subJsonEnable,
|
||||||
};
|
};
|
||||||
this.pageSize = pageSize;
|
this.pageSize = pageSize;
|
||||||
this.remarkModel = remarkModel;
|
this.remarkModel = remarkModel;
|
||||||
@@ -1433,15 +1434,19 @@
|
|||||||
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
|
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
|
||||||
return clientStats ? clientStats['enable'] : true;
|
return clientStats ? clientStats['enable'] : true;
|
||||||
},
|
},
|
||||||
// Returns true when client's traffic is exhausted or expiry time is passed
|
|
||||||
isClientDepleted(dbInbound, email) {
|
isClientDepleted(dbInbound, email) {
|
||||||
if (!email || !dbInbound || !dbInbound.clientStats) return false;
|
if (!email || !dbInbound || !dbInbound.clientStats) return false;
|
||||||
const stats = dbInbound.clientStats.find(s => s.email === email);
|
const stats = dbInbound.clientStats.find(s => s.email === email);
|
||||||
if (!stats) return false;
|
if (!stats) return false;
|
||||||
const now = new Date().getTime();
|
const total = stats.total ?? 0;
|
||||||
const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
|
const used = (stats.up ?? 0) + (stats.down ?? 0);
|
||||||
const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
|
const hasTotal = total > 0;
|
||||||
return exhausted || expired;
|
const exhausted = hasTotal && used >= total;
|
||||||
|
const expiryTime = stats.expiryTime ?? 0;
|
||||||
|
const hasExpiry = expiryTime > 0;
|
||||||
|
const now = Date.now();
|
||||||
|
const expired = hasExpiry && expiryTime <= now;
|
||||||
|
return expired || exhausted;
|
||||||
},
|
},
|
||||||
isClientOnline(email) {
|
isClientOnline(email) {
|
||||||
return this.onlineClients.includes(email);
|
return this.onlineClients.includes(email);
|
||||||
|
|||||||
@@ -431,12 +431,12 @@
|
|||||||
CPU History
|
CPU History
|
||||||
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
|
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
|
||||||
@change="fetchCpuHistoryBucket">
|
@change="fetchCpuHistoryBucket">
|
||||||
<a-select-option :value="2">2s</a-select-option>
|
<a-select-option :value="2">2m</a-select-option>
|
||||||
<a-select-option :value="30">30s</a-select-option>
|
<a-select-option :value="30">30m</a-select-option>
|
||||||
<a-select-option :value="60">1m</a-select-option>
|
<a-select-option :value="60">1h</a-select-option>
|
||||||
<a-select-option :value="120">2m</a-select-option>
|
<a-select-option :value="120">2h</a-select-option>
|
||||||
<a-select-option :value="180">3m</a-select-option>
|
<a-select-option :value="180">3h</a-select-option>
|
||||||
<a-select-option :value="300">5m</a-select-option>
|
<a-select-option :value="300">5h</a-select-option>
|
||||||
</a-select>
|
</a-select>
|
||||||
</template>
|
</template>
|
||||||
<div style="padding:16px">
|
<div style="padding:16px">
|
||||||
@@ -538,7 +538,7 @@
|
|||||||
const h = this.drawHeight
|
const h = this.drawHeight
|
||||||
const w = this.drawWidth
|
const w = this.drawWidth
|
||||||
// draw at 25%, 50%, 75%
|
// draw at 25%, 50%, 75%
|
||||||
return [0.25, 0.5, 0.75]
|
return [0, 0.25, 0.5, 0.75, 1]
|
||||||
.map(r => Math.round(this.paddingTop + h * r))
|
.map(r => Math.round(this.paddingTop + h * r))
|
||||||
.map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
|
.map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
|
||||||
},
|
},
|
||||||
@@ -622,7 +622,7 @@
|
|||||||
</g>
|
</g>
|
||||||
<!-- X ticks/labels -->
|
<!-- X ticks/labels -->
|
||||||
<g v-for="(t,i) in xTicks" :key="'x'+i">
|
<g v-for="(t,i) in xTicks" :key="'x'+i">
|
||||||
<text class="cpu-grid-x-text" :x="t.x" :y="paddingTop + drawHeight + 14" text-anchor="middle" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
|
<text class="cpu-grid-x-text" :x="t.x" :y="paddingTop + drawHeight + 22" text-anchor="middle" font-size="10" fill="rgba(0,0,0,0.3)" v-text="t.label"></text>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<path v-if="areaPath" :d="areaPath" fill="url(#spkGrad)" stroke="none" />
|
<path v-if="areaPath" :d="areaPath" fill="url(#spkGrad)" stroke="none" />
|
||||||
|
|||||||
@@ -180,9 +180,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td>{{ i18n "status" }}</td>
|
<td>{{ i18n "status" }}</td>
|
||||||
<td>
|
<td>
|
||||||
<a-tag v-if="isEnable && isActive && !isDepleted" color="green">{{ i18n "enabled" }}</a-tag>
|
|
||||||
<a-tag v-if="!isEnable && !isDepleted">{{ i18n "disabled" }}</a-tag>
|
|
||||||
<a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag>
|
<a-tag v-if="isDepleted" color="red">{{ i18n "depleted" }}</a-tag>
|
||||||
|
<a-tag v-else-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
|
||||||
|
<a-tag v-else>{{ i18n "disabled" }}</a-tag>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="infoModal.clientStats">
|
<tr v-if="infoModal.clientStats">
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
</tr-info-title>
|
</tr-info-title>
|
||||||
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
|
||||||
</tr-info-row>
|
</tr-info-row>
|
||||||
<tr-info-row class="tr-info-row">
|
<tr-info-row class="tr-info-row" v-if="app.subSettings.subJsonEnable">
|
||||||
<tr-info-title class="tr-info-title">
|
<tr-info-title class="tr-info-title">
|
||||||
<a-tag color="purple">Json Link</a-tag>
|
<a-tag color="purple">Json Link</a-tag>
|
||||||
<a-tooltip title='{{ i18n "copy" }}'>
|
<a-tooltip title='{{ i18n "copy" }}'>
|
||||||
@@ -524,7 +524,7 @@
|
|||||||
this.dbInbound = new DBInbound(dbInbound);
|
this.dbInbound = new DBInbound(dbInbound);
|
||||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||||
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
|
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
|
||||||
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
|
this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
[
|
[
|
||||||
@@ -548,7 +548,7 @@
|
|||||||
if (this.clientSettings) {
|
if (this.clientSettings) {
|
||||||
if (this.clientSettings.subId) {
|
if (this.clientSettings.subId) {
|
||||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||||
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
|
this.subJsonLink = app.subSettings.subJsonEnable ? this.genSubJsonLink(this.clientSettings.subId) : '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.visible = true;
|
this.visible = true;
|
||||||
@@ -588,11 +588,21 @@
|
|||||||
return infoModal.dbInbound.isEnable;
|
return infoModal.dbInbound.isEnable;
|
||||||
},
|
},
|
||||||
get isDepleted() {
|
get isDepleted() {
|
||||||
const stats = this.infoModal.clientStats;
|
const stats = infoModal.clientStats;
|
||||||
if (!stats) return false;
|
const settings = infoModal.clientSettings;
|
||||||
const now = new Date().getTime();
|
if (!stats || !settings) {
|
||||||
const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
|
return false;
|
||||||
const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
|
}
|
||||||
|
const total = stats.total ?? 0;
|
||||||
|
const used = (stats.up ?? 0) + (stats.down ?? 0);
|
||||||
|
const hasTotal = total > 0;
|
||||||
|
const exhausted = hasTotal && used >= total;
|
||||||
|
|
||||||
|
const expiryTime = settings.expiryTime ?? 0;
|
||||||
|
const hasExpiry = expiryTime > 0;
|
||||||
|
const now = Date.now();
|
||||||
|
const expired = hasExpiry && now >= expiryTime;
|
||||||
|
|
||||||
return expired || exhausted;
|
return expired || exhausted;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
</tr-qr-bg-inner>
|
</tr-qr-bg-inner>
|
||||||
</tr-qr-bg>
|
</tr-qr-bg>
|
||||||
</tr-qr-box>
|
</tr-qr-box>
|
||||||
<tr-qr-box class="qr-box">
|
<tr-qr-box class="qr-box" v-if="app.subSettings.subJsonEnable">
|
||||||
<a-tag color="purple" class="qr-tag"><span>{{ i18n "pages.settings.subSettings"}} Json</span></a-tag>
|
<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 class="qr-bg-sub">
|
||||||
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
<tr-qr-bg-inner class="qr-bg-sub-inner">
|
||||||
@@ -262,7 +262,9 @@
|
|||||||
if (qrModal.client && qrModal.client.subId) {
|
if (qrModal.client && qrModal.client.subId) {
|
||||||
qrModal.subId = qrModal.client.subId;
|
qrModal.subId = qrModal.client.subId;
|
||||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
if (app.subSettings.subJsonEnable) {
|
||||||
|
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
qrModal.qrcodes.forEach((element, index) => {
|
qrModal.qrcodes.forEach((element, index) => {
|
||||||
this.setQrCode("qrCode-" + index, element.link);
|
this.setQrCode("qrCode-" + index, element.link);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ template "settings/panel/subscription/general" . }}
|
{{ template "settings/panel/subscription/general" . }}
|
||||||
</a-tab-pane>
|
</a-tab-pane>
|
||||||
<a-tab-pane key="5" v-if="allSetting.subEnable" :style="{ paddingTop: '20px' }">
|
<a-tab-pane key="5" v-if="allSetting.subJsonEnable" :style="{ paddingTop: '20px' }">
|
||||||
<template #tab>
|
<template #tab>
|
||||||
<a-icon type="code"></a-icon>
|
<a-icon type="code"></a-icon>
|
||||||
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
|
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
|
||||||
@@ -523,6 +523,8 @@
|
|||||||
if (this.allSetting.subEnable) {
|
if (this.allSetting.subEnable) {
|
||||||
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
|
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
|
||||||
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
|
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
|
||||||
|
}
|
||||||
|
if (this.allSetting.subJsonEnable) {
|
||||||
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
|
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
|
||||||
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
|
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
<a-switch v-model="allSetting.subEnable"></a-switch>
|
<a-switch v-model="allSetting.subEnable"></a-switch>
|
||||||
</template>
|
</template>
|
||||||
</a-setting-list-item>
|
</a-setting-list-item>
|
||||||
|
<a-setting-list-item paddings="small">
|
||||||
|
<template #title>JSON Subscription</template>
|
||||||
|
<template #description>{{ i18n "pages.settings.subJsonEnable"}}</template>
|
||||||
|
<template #control>
|
||||||
|
<a-switch v-model="allSetting.subJsonEnable"></a-switch>
|
||||||
|
</template>
|
||||||
|
</a-setting-list-item>
|
||||||
<a-setting-list-item paddings="small">
|
<a-setting-list-item paddings="small">
|
||||||
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
|
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
|
||||||
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
|
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>
|
||||||
|
|||||||
@@ -56,7 +56,7 @@
|
|||||||
<a-space direction="vertical" align="center">
|
<a-space direction="vertical" align="center">
|
||||||
<a-row type="flex" :gutter="[8,8]"
|
<a-row type="flex" :gutter="[8,8]"
|
||||||
justify="center" style="width:100%">
|
justify="center" style="width:100%">
|
||||||
<a-col :xs="24" :sm="12"
|
<a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24"
|
||||||
style="text-align:center;">
|
style="text-align:center;">
|
||||||
<tr-qr-box class="qr-box">
|
<tr-qr-box class="qr-box">
|
||||||
<a-tag color="purple"
|
<a-tag color="purple"
|
||||||
@@ -75,7 +75,7 @@
|
|||||||
</tr-qr-bg>
|
</tr-qr-bg>
|
||||||
</tr-qr-box>
|
</tr-qr-box>
|
||||||
</a-col>
|
</a-col>
|
||||||
<a-col :xs="24" :sm="12"
|
<a-col v-if="app.subJsonUrl" :xs="24" :sm="12"
|
||||||
style="text-align:center;">
|
style="text-align:center;">
|
||||||
<tr-qr-box class="qr-box">
|
<tr-qr-box class="qr-box">
|
||||||
<a-tag color="purple"
|
<a-tag color="purple"
|
||||||
@@ -233,16 +233,17 @@
|
|||||||
<a-menu slot="overlay"
|
<a-menu slot="overlay"
|
||||||
:class="themeSwitcher.currentTheme">
|
:class="themeSwitcher.currentTheme">
|
||||||
<a-menu-item key="ios-shadowrocket"
|
<a-menu-item key="ios-shadowrocket"
|
||||||
@click="open('shadowrocket://add/subscription?url=' + encodeURIComponent(app.subUrl) + '&remark=' + encodeURIComponent(app.sId))">Shadowrocket</a-menu-item>
|
@click="open(shadowrocketUrl)">Shadowrocket</a-menu-item>
|
||||||
<a-menu-item key="ios-v2box"
|
<a-menu-item key="ios-v2box"
|
||||||
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
@click="open(v2boxUrl)">V2Box</a-menu-item>
|
||||||
<a-menu-item key="ios-streisand"
|
<a-menu-item key="ios-streisand"
|
||||||
@click="open('streisand://import/' + encodeURIComponent(app.subUrl))">Streisand</a-menu-item>
|
@click="open(streisandUrl)">Streisand</a-menu-item>
|
||||||
<a-menu-item key="ios-v2raytun"
|
<a-menu-item key="ios-v2raytun"
|
||||||
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
@click="copy(v2raytunUrl)">V2RayTun</a-menu-item>
|
||||||
<a-menu-item key="ios-npvtunnel"
|
<a-menu-item key="ios-npvtunnel"
|
||||||
@click="copy(app.subUrl)">NPV
|
@click="copy(npvtunUrl)">NPV
|
||||||
Tunnel</a-menu-item>
|
Tunnel
|
||||||
|
</a-menu-item>
|
||||||
</a-menu>
|
</a-menu>
|
||||||
</a-dropdown>
|
</a-dropdown>
|
||||||
</a-col>
|
</a-col>
|
||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckClientIpJob struct {
|
type CheckClientIpJob struct {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckHashStorageJob struct {
|
type CheckHashStorageJob struct {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type CheckXrayRunningJob struct {
|
type CheckXrayRunningJob struct {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ClearLogsJob struct{}
|
type ClearLogsJob struct{}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Period string
|
type Period string
|
||||||
@@ -20,12 +20,16 @@ func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
|
|||||||
|
|
||||||
func (j *PeriodicTrafficResetJob) Run() {
|
func (j *PeriodicTrafficResetJob) Run() {
|
||||||
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
|
||||||
logger.Infof("Running periodic traffic reset job for period: %s", j.period)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Failed to get inbounds for traffic reset:", err)
|
logger.Warning("Failed to get inbounds for traffic reset:", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(inbounds) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Infof("Running periodic traffic reset job for period: %s (%d matching inbounds)", j.period, len(inbounds))
|
||||||
|
|
||||||
resetCount := 0
|
resetCount := 0
|
||||||
|
|
||||||
for _, inbound := range inbounds {
|
for _, inbound := range inbounds {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package job
|
package job
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
)
|
)
|
||||||
|
|
||||||
type LoginStatus byte
|
type LoginStatus byte
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package job
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"x-ui/logger"
|
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
|
|
||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PanelService struct{}
|
type PanelService struct{}
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
"x-ui/util/sys"
|
"github.com/mhsanaei/3x-ui/util/sys"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/shirou/gopsutil/v4/cpu"
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
|
|||||||
@@ -10,14 +10,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
"x-ui/util/random"
|
"github.com/mhsanaei/3x-ui/util/random"
|
||||||
"x-ui/util/reflect_util"
|
"github.com/mhsanaei/3x-ui/util/reflect_util"
|
||||||
"x-ui/web/entity"
|
"github.com/mhsanaei/3x-ui/web/entity"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed config.json
|
//go:embed config.json
|
||||||
@@ -50,7 +50,8 @@ var defaultValueMap = map[string]string{
|
|||||||
"tgLang": "en-US",
|
"tgLang": "en-US",
|
||||||
"twoFactorEnable": "false",
|
"twoFactorEnable": "false",
|
||||||
"twoFactorToken": "",
|
"twoFactorToken": "",
|
||||||
"subEnable": "false",
|
"subEnable": "true",
|
||||||
|
"subJsonEnable": "false",
|
||||||
"subTitle": "",
|
"subTitle": "",
|
||||||
"subListen": "",
|
"subListen": "",
|
||||||
"subPort": "2096",
|
"subPort": "2096",
|
||||||
@@ -427,6 +428,10 @@ func (s *SettingService) GetSubEnable() (bool, error) {
|
|||||||
return s.getBool("subEnable")
|
return s.getBool("subEnable")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubJsonEnable() (bool, error) {
|
||||||
|
return s.getBool("subJsonEnable")
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetSubTitle() (string, error) {
|
func (s *SettingService) GetSubTitle() (string, error) {
|
||||||
return s.getString("subTitle")
|
return s.getString("subTitle")
|
||||||
}
|
}
|
||||||
@@ -575,6 +580,7 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
|||||||
"defaultKey": func() (any, error) { return s.GetKeyFile() },
|
"defaultKey": func() (any, error) { return s.GetKeyFile() },
|
||||||
"tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() },
|
"tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() },
|
||||||
"subEnable": func() (any, error) { return s.GetSubEnable() },
|
"subEnable": func() (any, error) { return s.GetSubEnable() },
|
||||||
|
"subJsonEnable": func() (any, error) { return s.GetSubJsonEnable() },
|
||||||
"subTitle": func() (any, error) { return s.GetSubTitle() },
|
"subTitle": func() (any, error) { return s.GetSubTitle() },
|
||||||
"subURI": func() (any, error) { return s.GetSubURI() },
|
"subURI": func() (any, error) { return s.GetSubURI() },
|
||||||
"subJsonURI": func() (any, error) { return s.GetSubJsonURI() },
|
"subJsonURI": func() (any, error) { return s.GetSubJsonURI() },
|
||||||
@@ -593,7 +599,14 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
|||||||
result[key] = value
|
result[key] = value
|
||||||
}
|
}
|
||||||
|
|
||||||
if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
|
subEnable := result["subEnable"].(bool)
|
||||||
|
subJsonEnable := false
|
||||||
|
if v, ok := result["subJsonEnable"]; ok {
|
||||||
|
if b, ok2 := v.(bool); ok2 {
|
||||||
|
subJsonEnable = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (subEnable && result["subURI"].(string) == "") || (subJsonEnable && result["subJsonURI"].(string) == "") {
|
||||||
subURI := ""
|
subURI := ""
|
||||||
subTitle, _ := s.GetSubTitle()
|
subTitle, _ := s.GetSubTitle()
|
||||||
subPort, _ := s.GetSubPort()
|
subPort, _ := s.GetSubPort()
|
||||||
@@ -619,13 +632,13 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
|
|||||||
} else {
|
} else {
|
||||||
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
|
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||||
}
|
}
|
||||||
if result["subURI"].(string) == "" {
|
if subEnable && result["subURI"].(string) == "" {
|
||||||
result["subURI"] = subURI + subPath
|
result["subURI"] = subURI + subPath
|
||||||
}
|
}
|
||||||
if result["subTitle"].(string) == "" {
|
if result["subTitle"].(string) == "" {
|
||||||
result["subTitle"] = subTitle
|
result["subTitle"] = subTitle
|
||||||
}
|
}
|
||||||
if result["subJsonURI"].(string) == "" {
|
if subJsonEnable && result["subJsonURI"].(string) == "" {
|
||||||
result["subJsonURI"] = subURI + subJsonPath
|
result["subJsonURI"] = subURI + subJsonPath
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
"x-ui/web/global"
|
"github.com/mhsanaei/3x-ui/web/global"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/web/locale"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/mymmrac/telego"
|
"github.com/mymmrac/telego"
|
||||||
@@ -1581,23 +1581,6 @@ 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(
|
||||||
@@ -1813,6 +1796,22 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||||||
t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove())
|
t.SendMsgToTgbot(chatId, msg, tu.ReplyKeyboardRemove())
|
||||||
|
|
||||||
}
|
}
|
||||||
|
default:
|
||||||
|
if after, ok := strings.CutPrefix(callbackQuery.Data, "client_sub_links "); ok {
|
||||||
|
email := after
|
||||||
|
t.sendClientSubLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if after, ok := strings.CutPrefix(callbackQuery.Data, "client_individual_links "); ok {
|
||||||
|
email := after
|
||||||
|
t.sendClientIndividualLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if after, ok := strings.CutPrefix(callbackQuery.Data, "client_qr_links "); ok {
|
||||||
|
email := after
|
||||||
|
t.sendClientQRLinks(chatId, email)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2093,6 +2092,7 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
|||||||
subPort, _ := t.settingService.GetSubPort()
|
subPort, _ := t.settingService.GetSubPort()
|
||||||
subPath, _ := t.settingService.GetSubPath()
|
subPath, _ := t.settingService.GetSubPath()
|
||||||
subJsonPath, _ := t.settingService.GetSubJsonPath()
|
subJsonPath, _ := t.settingService.GetSubJsonPath()
|
||||||
|
subJsonEnable, _ := t.settingService.GetSubJsonEnable()
|
||||||
subKeyFile, _ := t.settingService.GetSubKeyFile()
|
subKeyFile, _ := t.settingService.GetSubKeyFile()
|
||||||
subCertFile, _ := t.settingService.GetSubCertFile()
|
subCertFile, _ := t.settingService.GetSubCertFile()
|
||||||
|
|
||||||
@@ -2137,6 +2137,9 @@ func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
|||||||
|
|
||||||
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
||||||
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
||||||
|
if !subJsonEnable {
|
||||||
|
subJsonURL = ""
|
||||||
|
}
|
||||||
return subURL, subJsonURL, nil
|
return subURL, subJsonURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2146,8 +2149,10 @@ func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
|||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
msg := "Subscription URL:\r\n<code>" + subURL + "</code>\r\n\r\n" +
|
msg := "Subscription URL:\r\n<code>" + subURL + "</code>"
|
||||||
"JSON URL:\r\n<code>" + subJsonURL + "</code>"
|
if subJsonURL != "" {
|
||||||
|
msg += "\r\n\r\nJSON URL:\r\n<code>" + subJsonURL + "</code>"
|
||||||
|
}
|
||||||
inlineKeyboard := tu.InlineKeyboard(
|
inlineKeyboard := tu.InlineKeyboard(
|
||||||
tu.InlineKeyboardRow(
|
tu.InlineKeyboardRow(
|
||||||
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links "+email)),
|
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links "+email)),
|
||||||
@@ -2271,15 +2276,17 @@ func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
|||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send JSON URL QR (filename: subjson.png)
|
// Send JSON URL QR (filename: subjson.png) when available
|
||||||
if png, err := createQR(subJsonURL, 320); err == nil {
|
if subJsonURL != "" {
|
||||||
document := tu.Document(
|
if png, err := createQR(subJsonURL, 320); err == nil {
|
||||||
tu.ID(chatId),
|
document := tu.Document(
|
||||||
tu.FileFromBytes(png, "subjson.png"),
|
tu.ID(chatId),
|
||||||
)
|
tu.FileFromBytes(png, "subjson.png"),
|
||||||
_, _ = bot.SendDocument(context.Background(), document)
|
)
|
||||||
} else {
|
_, _ = bot.SendDocument(context.Background(), document)
|
||||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
} else {
|
||||||
|
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also generate a few individual links' QRs (first up to 5)
|
// Also generate a few individual links' QRs (first up to 5)
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ package service
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
"x-ui/database"
|
"github.com/mhsanaei/3x-ui/database"
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/crypto"
|
"github.com/mhsanaei/3x-ui/util/crypto"
|
||||||
|
|
||||||
"github.com/xlzd/gotp"
|
"github.com/xlzd/gotp"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
"x-ui/logger"
|
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
type WarpService struct {
|
type WarpService struct {
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
|
|
||||||
"go.uber.org/atomic"
|
"go.uber.org/atomic"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
"x-ui/xray"
|
"github.com/mhsanaei/3x-ui/xray"
|
||||||
)
|
)
|
||||||
|
|
||||||
type XraySettingService struct {
|
type XraySettingService struct {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"x-ui/database/model"
|
"github.com/mhsanaei/3x-ui/database/model"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "الاشتراك"
|
"subSettings" = "الاشتراك"
|
||||||
"subEnable" = "تفعيل خدمة الاشتراك"
|
"subEnable" = "تفعيل خدمة الاشتراك"
|
||||||
"subEnableDesc" = "يفعل خدمة الاشتراك."
|
"subEnableDesc" = "يفعل خدمة الاشتراك."
|
||||||
|
"subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل."
|
||||||
"subTitle" = "عنوان الاشتراك"
|
"subTitle" = "عنوان الاشتراك"
|
||||||
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
|
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
|
||||||
"subListen" = "IP الاستماع"
|
"subListen" = "IP الاستماع"
|
||||||
|
|||||||
@@ -369,8 +369,9 @@
|
|||||||
"timeZone" = "Time Zone"
|
"timeZone" = "Time Zone"
|
||||||
"timeZoneDesc" = "Scheduled tasks will run based on this time zone."
|
"timeZoneDesc" = "Scheduled tasks will run based on this time zone."
|
||||||
"subSettings" = "Subscription"
|
"subSettings" = "Subscription"
|
||||||
"subEnable" = "Enable Subscription Service"
|
"subEnable" = "Subscription Service"
|
||||||
"subEnableDesc" = "Enables the subscription service."
|
"subEnableDesc" = "Enable/Disable the subscription service."
|
||||||
|
"subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently."
|
||||||
"subTitle" = "Subscription Title"
|
"subTitle" = "Subscription Title"
|
||||||
"subTitleDesc" = "Title shown in VPN client"
|
"subTitleDesc" = "Title shown in VPN client"
|
||||||
"subListen" = "Listen IP"
|
"subListen" = "Listen IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "Suscripción"
|
"subSettings" = "Suscripción"
|
||||||
"subEnable" = "Habilitar Servicio"
|
"subEnable" = "Habilitar Servicio"
|
||||||
"subEnableDesc" = "Función de suscripción con configuración separada."
|
"subEnableDesc" = "Función de suscripción con configuración separada."
|
||||||
|
"subJsonEnable" = "Habilitar/Deshabilitar el endpoint de suscripción JSON de forma independiente."
|
||||||
"subTitle" = "Título de la Suscripción"
|
"subTitle" = "Título de la Suscripción"
|
||||||
"subTitleDesc" = "Título mostrado en el cliente de VPN"
|
"subTitleDesc" = "Título mostrado en el cliente de VPN"
|
||||||
"subListen" = "Listening IP"
|
"subListen" = "Listening IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "سابسکریپشن"
|
"subSettings" = "سابسکریپشن"
|
||||||
"subEnable" = "فعالسازی سرویس سابسکریپشن"
|
"subEnable" = "فعالسازی سرویس سابسکریپشن"
|
||||||
"subEnableDesc" = "سرویس سابسکریپشن را فعالمیکند"
|
"subEnableDesc" = "سرویس سابسکریپشن را فعالمیکند"
|
||||||
|
"subJsonEnable" = "فعال/غیرفعالسازی مستقل نقطه دسترسی سابسکریپشن JSON."
|
||||||
"subTitle" = "عنوان اشتراک"
|
"subTitle" = "عنوان اشتراک"
|
||||||
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
|
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
|
||||||
"subListen" = "آدرس آیپی"
|
"subListen" = "آدرس آیپی"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "Langganan"
|
"subSettings" = "Langganan"
|
||||||
"subEnable" = "Aktifkan Layanan Langganan"
|
"subEnable" = "Aktifkan Layanan Langganan"
|
||||||
"subEnableDesc" = "Mengaktifkan layanan langganan."
|
"subEnableDesc" = "Mengaktifkan layanan langganan."
|
||||||
|
"subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri."
|
||||||
"subTitle" = "Judul Langganan"
|
"subTitle" = "Judul Langganan"
|
||||||
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
|
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
|
||||||
"subListen" = "IP Pendengar"
|
"subListen" = "IP Pendengar"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "サブスクリプション設定"
|
"subSettings" = "サブスクリプション設定"
|
||||||
"subEnable" = "サブスクリプションサービスを有効にする"
|
"subEnable" = "サブスクリプションサービスを有効にする"
|
||||||
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
|
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
|
||||||
|
"subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。"
|
||||||
"subTitle" = "サブスクリプションタイトル"
|
"subTitle" = "サブスクリプションタイトル"
|
||||||
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
|
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
|
||||||
"subListen" = "監視IP"
|
"subListen" = "監視IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "Assinatura"
|
"subSettings" = "Assinatura"
|
||||||
"subEnable" = "Ativar Serviço de Assinatura"
|
"subEnable" = "Ativar Serviço de Assinatura"
|
||||||
"subEnableDesc" = "Ativa o serviço de assinatura."
|
"subEnableDesc" = "Ativa o serviço de assinatura."
|
||||||
|
"subJsonEnable" = "Ativar/Desativar o endpoint de assinatura JSON de forma independente."
|
||||||
"subTitle" = "Título da Assinatura"
|
"subTitle" = "Título da Assinatura"
|
||||||
"subTitleDesc" = "Título exibido no cliente VPN"
|
"subTitleDesc" = "Título exibido no cliente VPN"
|
||||||
"subListen" = "IP de Escuta"
|
"subListen" = "IP de Escuta"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "Подписка"
|
"subSettings" = "Подписка"
|
||||||
"subEnable" = "Включить подписку"
|
"subEnable" = "Включить подписку"
|
||||||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||||
|
"subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо."
|
||||||
"subTitle" = "Заголовок подписки"
|
"subTitle" = "Заголовок подписки"
|
||||||
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
|
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
|
||||||
"subListen" = "Прослушивание IP"
|
"subListen" = "Прослушивание IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "Abonelik"
|
"subSettings" = "Abonelik"
|
||||||
"subEnable" = "Abonelik Hizmetini Etkinleştir"
|
"subEnable" = "Abonelik Hizmetini Etkinleştir"
|
||||||
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
|
"subEnableDesc" = "Abonelik hizmetini etkinleştirir."
|
||||||
|
"subJsonEnable" = "JSON abonelik uç noktasını bağımsız olarak Etkinleştir/Devre Dışı bırak."
|
||||||
"subTitle" = "Abonelik Başlığı"
|
"subTitle" = "Abonelik Başlığı"
|
||||||
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
|
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
|
||||||
"subListen" = "Dinleme IP"
|
"subListen" = "Dinleme IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "Підписка"
|
"subSettings" = "Підписка"
|
||||||
"subEnable" = "Увімкнути службу підписки"
|
"subEnable" = "Увімкнути службу підписки"
|
||||||
"subEnableDesc" = "Вмикає службу підписки."
|
"subEnableDesc" = "Вмикає службу підписки."
|
||||||
|
"subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно."
|
||||||
"subTitle" = "Назва Підписки"
|
"subTitle" = "Назва Підписки"
|
||||||
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
|
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
|
||||||
"subListen" = "Слухати IP"
|
"subListen" = "Слухати IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "Gói đăng ký"
|
"subSettings" = "Gói đăng ký"
|
||||||
"subEnable" = "Bật dịch vụ"
|
"subEnable" = "Bật dịch vụ"
|
||||||
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
|
"subEnableDesc" = "Tính năng gói đăng ký với cấu hình riêng"
|
||||||
|
"subJsonEnable" = "Bật/Tắt điểm cuối đăng ký JSON độc lập."
|
||||||
"subTitle" = "Tiêu đề Đăng ký"
|
"subTitle" = "Tiêu đề Đăng ký"
|
||||||
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
|
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
|
||||||
"subListen" = "Listening IP"
|
"subListen" = "Listening IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "订阅设置"
|
"subSettings" = "订阅设置"
|
||||||
"subEnable" = "启用订阅服务"
|
"subEnable" = "启用订阅服务"
|
||||||
"subEnableDesc" = "启用订阅服务功能"
|
"subEnableDesc" = "启用订阅服务功能"
|
||||||
|
"subJsonEnable" = "单独启用/禁用 JSON 订阅端点。"
|
||||||
"subTitle" = "订阅标题"
|
"subTitle" = "订阅标题"
|
||||||
"subTitleDesc" = "在VPN客户端中显示的标题"
|
"subTitleDesc" = "在VPN客户端中显示的标题"
|
||||||
"subListen" = "监听 IP"
|
"subListen" = "监听 IP"
|
||||||
|
|||||||
@@ -371,6 +371,7 @@
|
|||||||
"subSettings" = "訂閱設定"
|
"subSettings" = "訂閱設定"
|
||||||
"subEnable" = "啟用訂閱服務"
|
"subEnable" = "啟用訂閱服務"
|
||||||
"subEnableDesc" = "啟用訂閱服務功能"
|
"subEnableDesc" = "啟用訂閱服務功能"
|
||||||
|
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
|
||||||
"subTitle" = "訂閱標題"
|
"subTitle" = "訂閱標題"
|
||||||
"subTitleDesc" = "在VPN客戶端中顯示的標題"
|
"subTitleDesc" = "在VPN客戶端中顯示的標題"
|
||||||
"subListen" = "監聽 IP"
|
"subListen" = "監聽 IP"
|
||||||
|
|||||||
18
web/web.go
18
web/web.go
@@ -14,15 +14,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
"x-ui/web/controller"
|
"github.com/mhsanaei/3x-ui/web/controller"
|
||||||
"x-ui/web/job"
|
"github.com/mhsanaei/3x-ui/web/job"
|
||||||
"x-ui/web/locale"
|
"github.com/mhsanaei/3x-ui/web/locale"
|
||||||
"x-ui/web/middleware"
|
"github.com/mhsanaei/3x-ui/web/middleware"
|
||||||
"x-ui/web/network"
|
"github.com/mhsanaei/3x-ui/web/network"
|
||||||
"x-ui/web/service"
|
"github.com/mhsanaei/3x-ui/web/service"
|
||||||
|
|
||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
"github.com/gin-contrib/sessions"
|
"github.com/gin-contrib/sessions"
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"regexp"
|
"regexp"
|
||||||
"time"
|
"time"
|
||||||
"math"
|
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
|
|
||||||
"github.com/xtls/xray-core/app/proxyman/command"
|
"github.com/xtls/xray-core/app/proxyman/command"
|
||||||
statsService "github.com/xtls/xray-core/app/stats/command"
|
statsService "github.com/xtls/xray-core/app/stats/command"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package xray
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/util/json_util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package xray
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
|
||||||
"x-ui/util/json_util"
|
"github.com/mhsanaei/3x-ui/util/json_util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type InboundConfig struct {
|
type InboundConfig struct {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
func NewLogWriter() *LogWriter {
|
func NewLogWriter() *LogWriter {
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"x-ui/config"
|
"github.com/mhsanaei/3x-ui/config"
|
||||||
"x-ui/logger"
|
"github.com/mhsanaei/3x-ui/logger"
|
||||||
"x-ui/util/common"
|
"github.com/mhsanaei/3x-ui/util/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetBinaryName() string {
|
func GetBinaryName() string {
|
||||||
|
|||||||
Reference in New Issue
Block a user