Compare commits

..

15 Commits

Author SHA1 Message Date
mhsanaei
0ffd27c0aa v2.8.2 2025-09-19 00:22:15 +02:00
mhsanaei
054cb1dea0 go package correction 2025-09-18 23:12:14 +02:00
Drahonn
3757ae0b11 cpu history timeframe (#3509) 2025-09-18 20:52:31 +02:00
mhsanaei
e3883fca87 donate: nowpayments 2025-09-18 20:14:10 +02:00
mhsanaei
b46a0b404b enhancements 2025-09-18 16:28:09 +02:00
mhsanaei
0ce58a095a vscode: Debug for developer 2025-09-18 14:33:51 +02:00
mhsanaei
59ea2645db new: subJsonEnable
after this subEnable by default is true
and subJsonEnable is false
2025-09-18 13:56:04 +02:00
mhsanaei
8c8d280f14 minor change 2025-09-18 12:20:21 +02:00
Harry NG
c720008187 chore: update sub page URL (#3505)
* Fix: Shadowrocket link using base64 encoding

* chore: update url
2025-09-18 12:11:52 +02:00
mhsanaei
170d24499e fix PeriodicTrafficResetJob: log only when there are matching inbound 2025-09-18 11:41:11 +02:00
mhsanaei
99c79d4056 fix: online 2025-09-17 20:02:58 +02:00
RahGozar
fcdeb1fc79 feat: add UUID to ClientTraffic (#3491)
* Update client_traffic.go

* Update inbound.go
2025-09-17 17:45:28 +02:00
Harry NG
0a58b5e745 Fix: Shadowrocket link using base64 encoding (#3489) 2025-09-17 17:43:09 +02:00
Tara Rostami
db7e7dcd29 css [fixes] (#3487) 2025-09-17 15:47:04 +02:00
mhsanaei
01b8a27996 bug fix 2025-09-17 15:46:03 +02:00
81 changed files with 480 additions and 294 deletions

2
.github/FUNDING.yml vendored
View File

@@ -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
polar: # Replace with a single Polar username
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
View 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
View 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"
}
]
}

View File

@@ -41,15 +41,13 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**إذا كان هذا المشروع مفيدًا لك، فقد ترغب في إعطائه**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
</a>
</br>
<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">
</a>
## النجوم عبر الزمن

View File

@@ -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:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
</a>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</br>
<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">
</a>
## Estrellas a lo Largo del Tiempo

View File

@@ -41,15 +41,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**اگر این پروژه برای شما مفید است، می‌توانید به آن یک**:star2: بدهید
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
</a>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</br>
<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">
</a>
## ستاره‌ها در طول زمان

View File

@@ -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:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
</a>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</br>
<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">
</a>
## Stargazers over Time

View File

@@ -41,15 +41,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**Если этот проект полезен для вас, вы можете поставить ему**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
</a>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</br>
<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">
</a>
## Звезды с течением времени

View File

@@ -41,15 +41,14 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
**如果这个项目对您有帮助,您可以给它一个**:star2:
<p align="left">
<a href="https://buymeacoffee.com/mhsanaei" target="_blank">
<img src="./media/buymeacoffe.png" alt="Image">
</a>
</p>
<a href="https://www.buymeacoffee.com/MHSanaei" target="_blank">
<img src="./media/default-yellow.png" alt="Buy Me A Coffee" style="height: 70px !important;width: 277px !important;" >
</a>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
</br>
<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">
</a>
## 随时间变化的星标数

View File

@@ -1 +1 @@
2.8.1
2.8.2

View File

@@ -9,10 +9,10 @@ import (
"path"
"slices"
"x-ui/config"
"x-ui/database/model"
"x-ui/util/crypto"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/config"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/util/crypto"
"github.com/mhsanaei/3x-ui/xray"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
@@ -141,6 +141,9 @@ func InitDB(dbPath string) error {
}
isUsersEmpty, err := isTableEmpty("users")
if err != nil {
return err
}
if err := initUser(); err != nil {
return err

View File

@@ -3,8 +3,8 @@ package model
import (
"fmt"
"x-ui/util/json_util"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/util/json_util"
"github.com/mhsanaei/3x-ui/xray"
)
type Protocol string

2
go.mod
View File

@@ -1,4 +1,4 @@
module x-ui
module github.com/mhsanaei/3x-ui
go 1.25.1

16
main.go
View File

@@ -9,14 +9,14 @@ import (
"syscall"
_ "unsafe"
"x-ui/config"
"x-ui/database"
"x-ui/logger"
"x-ui/sub"
"x-ui/util/crypto"
"x-ui/web"
"x-ui/web/global"
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/config"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/sub"
"github.com/mhsanaei/3x-ui/util/crypto"
"github.com/mhsanaei/3x-ui/web"
"github.com/mhsanaei/3x-ui/web/global"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/joho/godotenv"
"github.com/op/go-logging"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

BIN
media/default-yellow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -13,13 +13,13 @@ import (
"strconv"
"strings"
"x-ui/logger"
"x-ui/util/common"
webpkg "x-ui/web"
"x-ui/web/locale"
"x-ui/web/middleware"
"x-ui/web/network"
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
webpkg "github.com/mhsanaei/3x-ui/web"
"github.com/mhsanaei/3x-ui/web/locale"
"github.com/mhsanaei/3x-ui/web/middleware"
"github.com/mhsanaei/3x-ui/web/network"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/gin-gonic/gin"
)
@@ -30,7 +30,7 @@ func setEmbeddedTemplates(engine *gin.Engine) error {
webpkg.EmbeddedHTML(),
"html/common/page.html",
"html/component/aThemeSwitch.html",
"html/subscription.html",
"html/settings/panel/subscription/subpage.html",
)
if err != nil {
return err
@@ -85,6 +85,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
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
engine.Use(func(c *gin.Context) {
c.Set("base_path", LinksPath)
@@ -186,7 +192,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
g := engine.Group("/")
s.sub = NewSUBController(
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
return engine, nil
@@ -207,7 +213,7 @@ func (s *Server) getHtmlFiles() ([]string, error) {
files = append(files, theme)
}
// 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 {
files = append(files, page)
} else {

View File

@@ -4,7 +4,8 @@ import (
"encoding/base64"
"fmt"
"strings"
"x-ui/config"
"github.com/mhsanaei/3x-ui/config"
"github.com/gin-gonic/gin"
)
@@ -13,6 +14,7 @@ type SUBController struct {
subTitle string
subPath string
subJsonPath string
jsonEnabled bool
subEncrypt bool
updateInterval string
@@ -24,6 +26,7 @@ func NewSUBController(
g *gin.RouterGroup,
subPath string,
jsonPath string,
jsonEnabled bool,
encrypt bool,
showInfo bool,
rModel string,
@@ -39,6 +42,7 @@ func NewSUBController(
subTitle: subTitle,
subPath: subPath,
subJsonPath: jsonPath,
jsonEnabled: jsonEnabled,
subEncrypt: encrypt,
updateInterval: update,
@@ -51,10 +55,11 @@ func NewSUBController(
func (a *SUBController) initRouter(g *gin.RouterGroup) {
gLink := g.Group(a.subPath)
gJson := g.Group(a.subJsonPath)
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) {
@@ -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") {
// Build page data in service
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)
c.HTML(200, "subscription.html", gin.H{
c.HTML(200, "subpage.html", gin.H{
"title": "subscription.title",
"cur_ver": config.GetVersion(),
"host": page.Host,

View File

@@ -6,12 +6,12 @@ import (
"fmt"
"strings"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/json_util"
"x-ui/util/random"
"x-ui/web/service"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/json_util"
"github.com/mhsanaei/3x-ui/util/random"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/mhsanaei/3x-ui/xray"
)
//go:embed default.json

View File

@@ -11,13 +11,13 @@ import (
"github.com/gin-gonic/gin"
"github.com/goccy/go-json"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/util/random"
"x-ui/web/service"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/mhsanaei/3x-ui/util/random"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/mhsanaei/3x-ui/xray"
)
type SubService struct {
@@ -1007,7 +1007,7 @@ func searchHost(headers any) string {
return ""
}
// PageData is a view model for subscription.html
// PageData is a view model for subpage.html
type PageData struct {
Host string
BasePath string

View File

@@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"x-ui/logger"
"github.com/mhsanaei/3x-ui/logger"
)
func NewErrorf(format string, a ...any) error {

File diff suppressed because one or more lines are too long

View File

@@ -26,7 +26,8 @@ class AllSetting {
this.twoFactorEnable = false;
this.twoFactorToken = "";
this.xrayTemplateConfig = "";
this.subEnable = false;
this.subEnable = true;
this.subJsonEnable = false;
this.subTitle = "";
this.subListen = "";
this.subPort = 2096;

View File

@@ -50,7 +50,11 @@
}
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
@@ -61,22 +65,18 @@
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));
}
@@ -96,14 +96,16 @@
},
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
try {
const elJson = document.getElementById('qrcode-subjson');
if (elJson && this.app.subJsonUrl) {
new QRious({ element: elJson, value: this.app.subJsonUrl, size: 220 });
}
} catch (e) { /* ignore */ }
this._onResize = () => { this.viewportWidth = window.innerWidth; };
window.addEventListener('resize', this._onResize);
},
@@ -111,15 +113,45 @@
if (this._onResize) window.removeEventListener('resize', this._onResize);
},
computed: {
isMobile() { return this.viewportWidth < 576; },
isUnlimited() { return !this.app.totalByte; },
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;
},
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 + '" }}'; } },
});
})();

View File

@@ -1,7 +1,7 @@
package controller
import (
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/gin-gonic/gin"
)

View File

@@ -3,9 +3,9 @@ package controller
import (
"net/http"
"x-ui/logger"
"x-ui/web/locale"
"x-ui/web/session"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/web/locale"
"github.com/mhsanaei/3x-ui/web/session"
"github.com/gin-gonic/gin"
)

View File

@@ -5,9 +5,9 @@ import (
"fmt"
"strconv"
"x-ui/database/model"
"x-ui/web/service"
"x-ui/web/session"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/session"
"github.com/gin-gonic/gin"
)

View File

@@ -5,18 +5,18 @@ import (
"text/template"
"time"
"x-ui/logger"
"x-ui/web/service"
"x-ui/web/session"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/session"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
type LoginForm struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
TwoFactorCode string `json:"twoFactorCode" form:"twoFactorCode"`
}
type IndexController struct {

View File

@@ -7,8 +7,8 @@ import (
"strconv"
"time"
"x-ui/web/global"
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/global"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/gin-gonic/gin"
)

View File

@@ -4,10 +4,10 @@ import (
"errors"
"time"
"x-ui/util/crypto"
"x-ui/web/entity"
"x-ui/web/service"
"x-ui/web/session"
"github.com/mhsanaei/3x-ui/util/crypto"
"github.com/mhsanaei/3x-ui/web/entity"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/session"
"github.com/gin-gonic/gin"
)

View File

@@ -5,9 +5,9 @@ import (
"net/http"
"strings"
"x-ui/config"
"x-ui/logger"
"x-ui/web/entity"
"github.com/mhsanaei/3x-ui/config"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/web/entity"
"github.com/gin-gonic/gin"
)

View File

@@ -1,7 +1,7 @@
package controller
import (
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/gin-gonic/gin"
)

View File

@@ -7,7 +7,7 @@ import (
"strings"
"time"
"x-ui/util/common"
"github.com/mhsanaei/3x-ui/util/common"
)
type Msg struct {
@@ -42,6 +42,7 @@ type AllSetting struct {
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
SubEnable bool `json:"subEnable" form:"subEnable"`
SubJsonEnable bool `json:"subJsonEnable" form:"subJsonEnable"`
SubTitle string `json:"subTitle" form:"subTitle"`
SubListen string `json:"subListen" form:"subListen"`
SubPort int `json:"subPort" form:"subPort"`

View File

@@ -37,7 +37,7 @@
<template slot="content" >
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
</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>
</template>
<template v-else>
@@ -49,9 +49,9 @@
<a-space direction="horizontal" :size="2">
<a-tooltip>
<template slot="title">
<template v-if="isClientDepleted">{{ i18n "depleted" }}</template>
<template v-if="!isClientDepleted && !client.enable">{{ i18n "disabled" }}</template>
<template v-if="!isClientDepleted && client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
<template v-if="isClientDepleted(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
</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)" />
</td>
<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 v-else class="infinite-bar tr-table-bar">
<a-progress :show-info="false" :percent="100"></a-progress>
@@ -126,7 +126,7 @@
<tr class="tr-table-box">
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
<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 class="tr-table-lt">[[ client.reset + "d" ]]</td>
</tr>
@@ -213,7 +213,7 @@
</tr>
</table>
</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>
</td>
<td width="120px" v-else class="infinite-bar">
@@ -247,7 +247,7 @@
</template>
</span>
</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>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>

View File

@@ -568,8 +568,7 @@
<tr>
<td>{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}</td>
<td>
<a-tag color="blue">[[ i18n("pages.inbounds.periodicTrafficReset." +
dbInbound.trafficReset) ]]</a-tag>
<a-tag color="blue">[[ dbInbound.trafficReset ]]</a-tag>
</td>
</tr>
</table>
@@ -737,10 +736,11 @@
refreshing: false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: {
enable: false,
enable: true,
subTitle: '',
subURI: '',
subJsonURI: '',
subJsonEnable: false,
},
remarkModel: '-ieo',
datepicker: 'gregorian',
@@ -796,7 +796,8 @@
enable: subEnable,
subTitle: subTitle,
subURI: subURI,
subJsonURI: subJsonURI
subJsonURI: subJsonURI,
subJsonEnable: subJsonEnable,
};
this.pageSize = pageSize;
this.remarkModel = remarkModel;
@@ -1433,15 +1434,19 @@
clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null;
return clientStats ? clientStats['enable'] : true;
},
// Returns true when client's traffic is exhausted or expiry time is passed
isClientDepleted(dbInbound, email) {
if (!email || !dbInbound || !dbInbound.clientStats) return false;
const stats = dbInbound.clientStats.find(s => s.email === email);
if (!stats) return false;
const now = new Date().getTime();
const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
return exhausted || expired;
const total = stats.total ?? 0;
const used = (stats.up ?? 0) + (stats.down ?? 0);
const hasTotal = total > 0;
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) {
return this.onlineClients.includes(email);

View File

@@ -431,12 +431,12 @@
CPU History
<a-select size="small" v-model="cpuHistoryModal.bucket" class="ml-10" style="width: 80px"
@change="fetchCpuHistoryBucket">
<a-select-option :value="2">2s</a-select-option>
<a-select-option :value="30">30s</a-select-option>
<a-select-option :value="60">1m</a-select-option>
<a-select-option :value="120">2m</a-select-option>
<a-select-option :value="180">3m</a-select-option>
<a-select-option :value="300">5m</a-select-option>
<a-select-option :value="2">2m</a-select-option>
<a-select-option :value="30">30m</a-select-option>
<a-select-option :value="60">1h</a-select-option>
<a-select-option :value="120">2h</a-select-option>
<a-select-option :value="180">3h</a-select-option>
<a-select-option :value="300">5h</a-select-option>
</a-select>
</template>
<div style="padding:16px">
@@ -538,7 +538,7 @@
const h = this.drawHeight
const w = this.drawWidth
// 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(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
},
@@ -622,7 +622,7 @@
</g>
<!-- X ticks/labels -->
<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>
<path v-if="areaPath" :d="areaPath" fill="url(#spkGrad)" stroke="none" />
@@ -1124,4 +1124,4 @@
},
});
</script>
{{ template "page/body_end" .}}
{{ template "page/body_end" .}}

View File

@@ -180,9 +180,9 @@
<tr>
<td>{{ i18n "status" }}</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-else-if="isEnable" color="green">{{ i18n "enabled" }}</a-tag>
<a-tag v-else>{{ i18n "disabled" }}</a-tag>
</td>
</tr>
<tr v-if="infoModal.clientStats">
@@ -308,7 +308,7 @@
</tr-info-title>
<a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a>
</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">
<a-tag color="purple">Json Link</a-tag>
<a-tooltip title='{{ i18n "copy" }}'>
@@ -524,7 +524,7 @@
this.dbInbound = new DBInbound(dbInbound);
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
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 (
[
@@ -548,7 +548,7 @@
if (this.clientSettings) {
if (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;
@@ -588,11 +588,21 @@
return infoModal.dbInbound.isEnable;
},
get isDepleted() {
const stats = this.infoModal.clientStats;
if (!stats) return false;
const now = new Date().getTime();
const expired = stats.expiryTime > 0 && now >= stats.expiryTime;
const exhausted = stats.total > 0 && (stats.up + stats.down) >= stats.total;
const stats = infoModal.clientStats;
const settings = infoModal.clientSettings;
if (!stats || !settings) {
return false;
}
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;
},
},

View File

@@ -30,7 +30,7 @@
</tr-qr-bg-inner>
</tr-qr-bg>
</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>
<tr-qr-bg class="qr-bg-sub">
<tr-qr-bg-inner class="qr-bg-sub-inner">
@@ -262,7 +262,9 @@
if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.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) => {
this.setQrCode("qrCode-" + index, element.link);

View File

@@ -79,7 +79,7 @@
</template>
{{ template "settings/panel/subscription/general" . }}
</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>
<a-icon type="code"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }} (JSON)</span>
@@ -523,6 +523,8 @@
if (this.allSetting.subEnable) {
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
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;
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
}

View File

@@ -8,6 +8,13 @@
<a-switch v-model="allSetting.subEnable"></a-switch>
</template>
</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">
<template #title>{{ i18n "pages.settings.subTitle"}}</template>
<template #description>{{ i18n "pages.settings.subTitleDesc"}}</template>

View File

@@ -56,7 +56,7 @@
<a-space direction="vertical" align="center">
<a-row type="flex" :gutter="[8,8]"
justify="center" style="width:100%">
<a-col :xs="24" :sm="12"
<a-col :xs="24" :sm="app.subJsonUrl ? 12 : 24"
style="text-align:center;">
<tr-qr-box class="qr-box">
<a-tag color="purple"
@@ -75,7 +75,7 @@
</tr-qr-bg>
</tr-qr-box>
</a-col>
<a-col :xs="24" :sm="12"
<a-col v-if="app.subJsonUrl" :xs="24" :sm="12"
style="text-align:center;">
<tr-qr-box class="qr-box">
<a-tag color="purple"
@@ -233,16 +233,17 @@
<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>
@click="open(shadowrocketUrl)">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>
@click="open(v2boxUrl)">V2Box</a-menu-item>
<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"
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
@click="copy(v2raytunUrl)">V2RayTun</a-menu-item>
<a-menu-item key="ios-npvtunnel"
@click="copy(app.subUrl)">NPV
Tunnel</a-menu-item>
@click="copy(npvtunUrl)">NPV
Tunnel
</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>

View File

@@ -12,10 +12,10 @@ import (
"sort"
"time"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/xray"
)
type CheckClientIpJob struct {

View File

@@ -4,7 +4,7 @@ import (
"strconv"
"time"
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/shirou/gopsutil/v4/cpu"
)

View File

@@ -1,7 +1,7 @@
package job
import (
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/service"
)
type CheckHashStorageJob struct {

View File

@@ -1,8 +1,8 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/web/service"
)
type CheckXrayRunningJob struct {

View File

@@ -5,8 +5,8 @@ import (
"os"
"path/filepath"
"x-ui/logger"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/xray"
)
type ClearLogsJob struct{}

View File

@@ -1,8 +1,8 @@
package job
import (
"x-ui/logger"
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/web/service"
)
type Period string
@@ -20,12 +20,16 @@ func NewPeriodicTrafficResetJob(period Period) *PeriodicTrafficResetJob {
func (j *PeriodicTrafficResetJob) Run() {
inbounds, err := j.inboundService.GetInboundsByTrafficReset(string(j.period))
logger.Infof("Running periodic traffic reset job for period: %s", j.period)
if err != nil {
logger.Warning("Failed to get inbounds for traffic reset:", err)
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
for _, inbound := range inbounds {

View File

@@ -1,7 +1,7 @@
package job
import (
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/web/service"
)
type LoginStatus byte

View File

@@ -2,9 +2,10 @@ package job
import (
"encoding/json"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/mhsanaei/3x-ui/xray"
"github.com/valyala/fasthttp"
)

View File

@@ -6,7 +6,7 @@ import (
"os"
"strings"
"x-ui/logger"
"github.com/mhsanaei/3x-ui/logger"
"github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n"

View File

@@ -8,11 +8,11 @@ import (
"strings"
"time"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/mhsanaei/3x-ui/xray"
"gorm.io/gorm"
)

View File

@@ -1,10 +1,10 @@
package service
import (
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/xray"
"gorm.io/gorm"
)

View File

@@ -5,7 +5,7 @@ import (
"syscall"
"time"
"x-ui/logger"
"github.com/mhsanaei/3x-ui/logger"
)
type PanelService struct{}

View File

@@ -19,12 +19,12 @@ import (
"sync"
"time"
"x-ui/config"
"x-ui/database"
"x-ui/logger"
"x-ui/util/common"
"x-ui/util/sys"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/config"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/mhsanaei/3x-ui/util/sys"
"github.com/mhsanaei/3x-ui/xray"
"github.com/google/uuid"
"github.com/shirou/gopsutil/v4/cpu"

View File

@@ -10,14 +10,14 @@ import (
"strings"
"time"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/util/random"
"x-ui/util/reflect_util"
"x-ui/web/entity"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/mhsanaei/3x-ui/util/random"
"github.com/mhsanaei/3x-ui/util/reflect_util"
"github.com/mhsanaei/3x-ui/web/entity"
"github.com/mhsanaei/3x-ui/xray"
)
//go:embed config.json
@@ -50,7 +50,8 @@ var defaultValueMap = map[string]string{
"tgLang": "en-US",
"twoFactorEnable": "false",
"twoFactorToken": "",
"subEnable": "false",
"subEnable": "true",
"subJsonEnable": "false",
"subTitle": "",
"subListen": "",
"subPort": "2096",
@@ -427,6 +428,10 @@ func (s *SettingService) GetSubEnable() (bool, error) {
return s.getBool("subEnable")
}
func (s *SettingService) GetSubJsonEnable() (bool, error) {
return s.getBool("subJsonEnable")
}
func (s *SettingService) GetSubTitle() (string, error) {
return s.getString("subTitle")
}
@@ -575,6 +580,7 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
"defaultKey": func() (any, error) { return s.GetKeyFile() },
"tgBotEnable": func() (any, error) { return s.GetTgbotEnabled() },
"subEnable": func() (any, error) { return s.GetSubEnable() },
"subJsonEnable": func() (any, error) { return s.GetSubJsonEnable() },
"subTitle": func() (any, error) { return s.GetSubTitle() },
"subURI": func() (any, error) { return s.GetSubURI() },
"subJsonURI": func() (any, error) { return s.GetSubJsonURI() },
@@ -593,7 +599,14 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
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 := ""
subTitle, _ := s.GetSubTitle()
subPort, _ := s.GetSubPort()
@@ -619,13 +632,13 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) {
} else {
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
}
if result["subURI"].(string) == "" {
if subEnable && result["subURI"].(string) == "" {
result["subURI"] = subURI + subPath
}
if result["subTitle"].(string) == "" {
result["subTitle"] = subTitle
}
if result["subJsonURI"].(string) == "" {
if subJsonEnable && result["subJsonURI"].(string) == "" {
result["subJsonURI"] = subURI + subJsonPath
}
}

View File

@@ -18,14 +18,14 @@ import (
"strings"
"time"
"x-ui/config"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/global"
"x-ui/web/locale"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/config"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/mhsanaei/3x-ui/web/global"
"github.com/mhsanaei/3x-ui/web/locale"
"github.com/mhsanaei/3x-ui/xray"
"github.com/google/uuid"
"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)
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":
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
@@ -1813,6 +1796,22 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
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()
subPath, _ := t.settingService.GetSubPath()
subJsonPath, _ := t.settingService.GetSubJsonPath()
subJsonEnable, _ := t.settingService.GetSubJsonEnable()
subKeyFile, _ := t.settingService.GetSubKeyFile()
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)
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
if !subJsonEnable {
subJsonURL = ""
}
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())
return
}
msg := "Subscription URL:\r\n<code>" + subURL + "</code>\r\n\r\n" +
"JSON URL:\r\n<code>" + subJsonURL + "</code>"
msg := "Subscription URL:\r\n<code>" + subURL + "</code>"
if subJsonURL != "" {
msg += "\r\n\r\nJSON URL:\r\n<code>" + subJsonURL + "</code>"
}
inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow(
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())
}
// 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())
// Send JSON URL QR (filename: subjson.png) when available
if subJsonURL != "" {
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)

View File

@@ -3,10 +3,10 @@ package service
import (
"errors"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/crypto"
"github.com/mhsanaei/3x-ui/database"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/crypto"
"github.com/xlzd/gotp"
"gorm.io/gorm"

View File

@@ -7,8 +7,9 @@ import (
"net/http"
"os"
"time"
"x-ui/logger"
"x-ui/util/common"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
)
type WarpService struct {

View File

@@ -6,8 +6,8 @@ import (
"runtime"
"sync"
"x-ui/logger"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/xray"
"go.uber.org/atomic"
)

View File

@@ -4,8 +4,8 @@ import (
_ "embed"
"encoding/json"
"x-ui/util/common"
"x-ui/xray"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/mhsanaei/3x-ui/xray"
)
type XraySettingService struct {

View File

@@ -4,7 +4,7 @@ import (
"encoding/gob"
"net/http"
"x-ui/database/model"
"github.com/mhsanaei/3x-ui/database/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"

View File

@@ -371,6 +371,7 @@
"subSettings" = "الاشتراك"
"subEnable" = "تفعيل خدمة الاشتراك"
"subEnableDesc" = "يفعل خدمة الاشتراك."
"subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل."
"subTitle" = "عنوان الاشتراك"
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
"subListen" = "IP الاستماع"

View File

@@ -369,8 +369,9 @@
"timeZone" = "Time Zone"
"timeZoneDesc" = "Scheduled tasks will run based on this time zone."
"subSettings" = "Subscription"
"subEnable" = "Enable Subscription Service"
"subEnableDesc" = "Enables the subscription service."
"subEnable" = "Subscription Service"
"subEnableDesc" = "Enable/Disable the subscription service."
"subJsonEnable" = "Enable/Disable the JSON subscription endpoint independently."
"subTitle" = "Subscription Title"
"subTitleDesc" = "Title shown in VPN client"
"subListen" = "Listen IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "Suscripción"
"subEnable" = "Habilitar Servicio"
"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"
"subTitleDesc" = "Título mostrado en el cliente de VPN"
"subListen" = "Listening IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "سابسکریپشن"
"subEnable" = "فعال‌سازی سرویس سابسکریپشن"
"subEnableDesc" = "سرویس سابسکریپشن‌ را فعال‌می‌کند"
"subJsonEnable" = "فعال/غیرفعال‌سازی مستقل نقطه دسترسی سابسکریپشن JSON."
"subTitle" = "عنوان اشتراک"
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
"subListen" = "آدرس آی‌پی"

View File

@@ -371,6 +371,7 @@
"subSettings" = "Langganan"
"subEnable" = "Aktifkan Layanan Langganan"
"subEnableDesc" = "Mengaktifkan layanan langganan."
"subJsonEnable" = "Aktifkan/Nonaktifkan endpoint langganan JSON secara mandiri."
"subTitle" = "Judul Langganan"
"subTitleDesc" = "Judul yang ditampilkan di klien VPN"
"subListen" = "IP Pendengar"

View File

@@ -371,6 +371,7 @@
"subSettings" = "サブスクリプション設定"
"subEnable" = "サブスクリプションサービスを有効にする"
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
"subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。"
"subTitle" = "サブスクリプションタイトル"
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
"subListen" = "監視IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "Assinatura"
"subEnable" = "Ativar 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"
"subTitleDesc" = "Título exibido no cliente VPN"
"subListen" = "IP de Escuta"

View File

@@ -371,6 +371,7 @@
"subSettings" = "Подписка"
"subEnable" = "Включить подписку"
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
"subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо."
"subTitle" = "Заголовок подписки"
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
"subListen" = "Прослушивание IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "Abonelik"
"subEnable" = "Abonelik Hizmetini Etkinleştir"
"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ığı"
"subTitleDesc" = "VPN istemcisinde gösterilen başlık"
"subListen" = "Dinleme IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "Підписка"
"subEnable" = "Увімкнути службу підписки"
"subEnableDesc" = "Вмикає службу підписки."
"subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно."
"subTitle" = "Назва Підписки"
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
"subListen" = "Слухати IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "Gói đăng ký"
"subEnable" = "Bật dịch vụ"
"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ý"
"subTitleDesc" = "Tiêu đề hiển thị trong ứng dụng VPN"
"subListen" = "Listening IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "订阅设置"
"subEnable" = "启用订阅服务"
"subEnableDesc" = "启用订阅服务功能"
"subJsonEnable" = "单独启用/禁用 JSON 订阅端点。"
"subTitle" = "订阅标题"
"subTitleDesc" = "在VPN客户端中显示的标题"
"subListen" = "监听 IP"

View File

@@ -371,6 +371,7 @@
"subSettings" = "訂閱設定"
"subEnable" = "啟用訂閱服務"
"subEnableDesc" = "啟用訂閱服務功能"
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
"subTitle" = "訂閱標題"
"subTitleDesc" = "在VPN客戶端中顯示的標題"
"subListen" = "監聽 IP"

View File

@@ -14,15 +14,15 @@ import (
"strings"
"time"
"x-ui/config"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/controller"
"x-ui/web/job"
"x-ui/web/locale"
"x-ui/web/middleware"
"x-ui/web/network"
"x-ui/web/service"
"github.com/mhsanaei/3x-ui/config"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/mhsanaei/3x-ui/web/controller"
"github.com/mhsanaei/3x-ui/web/job"
"github.com/mhsanaei/3x-ui/web/locale"
"github.com/mhsanaei/3x-ui/web/middleware"
"github.com/mhsanaei/3x-ui/web/network"
"github.com/mhsanaei/3x-ui/web/service"
"github.com/gin-contrib/gzip"
"github.com/gin-contrib/sessions"

View File

@@ -4,12 +4,12 @@ import (
"context"
"encoding/json"
"fmt"
"math"
"regexp"
"time"
"math"
"x-ui/logger"
"x-ui/util/common"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
"github.com/xtls/xray-core/app/proxyman/command"
statsService "github.com/xtls/xray-core/app/stats/command"

View File

@@ -3,7 +3,7 @@ package xray
import (
"bytes"
"x-ui/util/json_util"
"github.com/mhsanaei/3x-ui/util/json_util"
)
type Config struct {

View File

@@ -3,7 +3,7 @@ package xray
import (
"bytes"
"x-ui/util/json_util"
"github.com/mhsanaei/3x-ui/util/json_util"
)
type InboundConfig struct {

View File

@@ -5,7 +5,7 @@ import (
"runtime"
"strings"
"x-ui/logger"
"github.com/mhsanaei/3x-ui/logger"
)
func NewLogWriter() *LogWriter {

View File

@@ -13,9 +13,9 @@ import (
"syscall"
"time"
"x-ui/config"
"x-ui/logger"
"x-ui/util/common"
"github.com/mhsanaei/3x-ui/config"
"github.com/mhsanaei/3x-ui/logger"
"github.com/mhsanaei/3x-ui/util/common"
)
func GetBinaryName() string {