mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-26 18:14:50 +00:00
Compare commits
7 commits
b2ad033bfc
...
56d1b75021
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56d1b75021 | ||
|
|
b46a0b404b | ||
|
|
0ce58a095a | ||
|
|
59ea2645db | ||
|
|
8c8d280f14 | ||
|
|
c720008187 | ||
|
|
170d24499e |
31 changed files with 233 additions and 62 deletions
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
sub/sub.go
12
sub/sub.go
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ type SUBController struct {
|
|||
subTitle string
|
||||
subPath string
|
||||
subJsonPath string
|
||||
jsonEnabled bool
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
|
|
@ -24,6 +25,7 @@ func NewSUBController(
|
|||
g *gin.RouterGroup,
|
||||
subPath string,
|
||||
jsonPath string,
|
||||
jsonEnabled bool,
|
||||
encrypt bool,
|
||||
showInfo bool,
|
||||
rModel string,
|
||||
|
|
@ -39,6 +41,7 @@ func NewSUBController(
|
|||
subTitle: subTitle,
|
||||
subPath: subPath,
|
||||
subJsonPath: jsonPath,
|
||||
jsonEnabled: jsonEnabled,
|
||||
subEncrypt: encrypt,
|
||||
updateInterval: update,
|
||||
|
||||
|
|
@ -51,10 +54,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 +78,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,
|
||||
|
|
|
|||
|
|
@ -1028,7 +1028,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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -101,7 +101,10 @@
|
|||
if (sj) this.app.subJsonUrl = sj;
|
||||
drawQR(this.app.subUrl);
|
||||
try {
|
||||
new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 });
|
||||
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);
|
||||
|
|
@ -127,6 +130,18 @@
|
|||
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: {
|
||||
|
|
|
|||
|
|
@ -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"`
|
||||
|
|
|
|||
|
|
@ -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="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>
|
||||
|
|
|
|||
|
|
@ -736,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',
|
||||
|
|
@ -795,7 +796,8 @@
|
|||
enable: subEnable,
|
||||
subTitle: subTitle,
|
||||
subURI: subURI,
|
||||
subJsonURI: subJsonURI
|
||||
subJsonURI: subJsonURI,
|
||||
subJsonEnable: subJsonEnable,
|
||||
};
|
||||
this.pageSize = pageSize;
|
||||
this.remarkModel = remarkModel;
|
||||
|
|
@ -1432,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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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" }}');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -235,14 +235,15 @@
|
|||
<a-menu-item key="ios-shadowrocket"
|
||||
@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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -442,6 +443,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")
|
||||
}
|
||||
|
|
@ -590,6 +595,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() },
|
||||
|
|
@ -608,7 +614,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()
|
||||
|
|
@ -634,13 +647,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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2093,6 +2093,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 +2138,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 +2150,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 +2277,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)
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@
|
|||
"subSettings" = "الاشتراك"
|
||||
"subEnable" = "تفعيل خدمة الاشتراك"
|
||||
"subEnableDesc" = "يفعل خدمة الاشتراك."
|
||||
"subJsonEnable" = "تمكين/تعطيل نقطة نهاية اشتراك JSON بشكل مستقل."
|
||||
"subTitle" = "عنوان الاشتراك"
|
||||
"subTitleDesc" = "العنوان اللي هيظهر في عميل VPN"
|
||||
"subListen" = "IP الاستماع"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@
|
|||
"subSettings" = "سابسکریپشن"
|
||||
"subEnable" = "فعالسازی سرویس سابسکریپشن"
|
||||
"subEnableDesc" = "سرویس سابسکریپشن را فعالمیکند"
|
||||
"subJsonEnable" = "فعال/غیرفعالسازی مستقل نقطه دسترسی سابسکریپشن JSON."
|
||||
"subTitle" = "عنوان اشتراک"
|
||||
"subTitleDesc" = "عنوان نمایش داده شده در کلاینت VPN"
|
||||
"subListen" = "آدرس آیپی"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@
|
|||
"subSettings" = "サブスクリプション設定"
|
||||
"subEnable" = "サブスクリプションサービスを有効にする"
|
||||
"subEnableDesc" = "サブスクリプションサービス機能を有効にする"
|
||||
"subJsonEnable" = "JSON サブスクリプションのエンドポイントを個別に有効/無効にする。"
|
||||
"subTitle" = "サブスクリプションタイトル"
|
||||
"subTitleDesc" = "VPNクライアントに表示されるタイトル"
|
||||
"subListen" = "監視IP"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@
|
|||
"subSettings" = "Подписка"
|
||||
"subEnable" = "Включить подписку"
|
||||
"subEnableDesc" = "Функция подписки с отдельной конфигурацией"
|
||||
"subJsonEnable" = "Включить/отключить JSON-эндпоинт подписки независимо."
|
||||
"subTitle" = "Заголовок подписки"
|
||||
"subTitleDesc" = "Название подписки, которое видит клиент в VPN клиенте"
|
||||
"subListen" = "Прослушивание IP"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@
|
|||
"subSettings" = "Підписка"
|
||||
"subEnable" = "Увімкнути службу підписки"
|
||||
"subEnableDesc" = "Вмикає службу підписки."
|
||||
"subJsonEnable" = "Увімкнути/вимкнути JSON-кінець підписки незалежно."
|
||||
"subTitle" = "Назва Підписки"
|
||||
"subTitleDesc" = "Назва, яка відображається у VPN-клієнті"
|
||||
"subListen" = "Слухати IP"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@
|
|||
"subSettings" = "订阅设置"
|
||||
"subEnable" = "启用订阅服务"
|
||||
"subEnableDesc" = "启用订阅服务功能"
|
||||
"subJsonEnable" = "单独启用/禁用 JSON 订阅端点。"
|
||||
"subTitle" = "订阅标题"
|
||||
"subTitleDesc" = "在VPN客户端中显示的标题"
|
||||
"subListen" = "监听 IP"
|
||||
|
|
|
|||
|
|
@ -371,6 +371,7 @@
|
|||
"subSettings" = "訂閱設定"
|
||||
"subEnable" = "啟用訂閱服務"
|
||||
"subEnableDesc" = "啟用訂閱服務功能"
|
||||
"subJsonEnable" = "獨立啟用/停用 JSON 訂閱端點。"
|
||||
"subTitle" = "訂閱標題"
|
||||
"subTitleDesc" = "在VPN客戶端中顯示的標題"
|
||||
"subListen" = "監聽 IP"
|
||||
|
|
|
|||
Loading…
Reference in a new issue