mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-27 10:30:08 +00:00
Compare commits
No commits in common. "b2b002464826e5ef6d6864756e7ec9beb431d7e4" and "104526aab25b58caa497e17e44c38811a19de9ed" have entirely different histories.
b2b0024648
...
104526aab2
5 changed files with 58 additions and 106 deletions
8
.github/workflows/docker.yml
vendored
8
.github/workflows/docker.yml
vendored
|
|
@ -1,9 +1,7 @@
|
||||||
name: Release 3X-UI for Docker
|
name: Release 3X-UI for Docker
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
push:
|
push:
|
||||||
|
|
@ -29,7 +27,7 @@ jobs:
|
||||||
tags: |
|
tags: |
|
||||||
type=ref,event=branch
|
type=ref,event=branch
|
||||||
type=ref,event=tag
|
type=ref,event=tag
|
||||||
type=semver,pattern={{version}}
|
type=pep440,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
@ -49,7 +47,7 @@ jobs:
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
|
|
@ -57,6 +55,6 @@ jobs:
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
50
sub/sub.go
50
sub/sub.go
|
|
@ -98,14 +98,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set base_path based on LinksPath for template rendering
|
// Set base_path based on LinksPath for template rendering
|
||||||
// Ensure LinksPath ends with "/" for proper asset URL generation
|
|
||||||
basePath := LinksPath
|
|
||||||
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
|
|
||||||
basePath += "/"
|
|
||||||
}
|
|
||||||
logger.Debug("sub: Setting base_path to:", basePath)
|
|
||||||
engine.Use(func(c *gin.Context) {
|
engine.Use(func(c *gin.Context) {
|
||||||
c.Set("base_path", basePath)
|
c.Set("base_path", LinksPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||||
|
|
@ -185,48 +179,22 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mount assets in multiple paths to handle different URL patterns
|
|
||||||
var assetsFS http.FileSystem
|
|
||||||
if _, err := os.Stat("web/assets"); err == nil {
|
if _, err := os.Stat("web/assets"); err == nil {
|
||||||
assetsFS = http.FS(os.DirFS("web/assets"))
|
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
||||||
|
if linksPathForAssets != "/assets" {
|
||||||
|
engine.StaticFS(linksPathForAssets, http.FS(os.DirFS("web/assets")))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||||
assetsFS = http.FS(subFS)
|
engine.StaticFS("/assets", http.FS(subFS))
|
||||||
|
if linksPathForAssets != "/assets" {
|
||||||
|
engine.StaticFS(linksPathForAssets, http.FS(subFS))
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
logger.Error("sub: failed to mount embedded assets:", err)
|
logger.Error("sub: failed to mount embedded assets:", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if assetsFS != nil {
|
|
||||||
engine.StaticFS("/assets", assetsFS)
|
|
||||||
if linksPathForAssets != "/assets" {
|
|
||||||
engine.StaticFS(linksPathForAssets, assetsFS)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add middleware to handle dynamic asset paths with subid
|
|
||||||
if LinksPath != "/" {
|
|
||||||
engine.Use(func(c *gin.Context) {
|
|
||||||
path := c.Request.URL.Path
|
|
||||||
// Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
|
|
||||||
pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
|
|
||||||
if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
|
|
||||||
// Extract the asset path after /assets/
|
|
||||||
assetsIndex := strings.Index(path, "/assets/")
|
|
||||||
if assetsIndex != -1 {
|
|
||||||
assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
|
|
||||||
if assetPath != "" {
|
|
||||||
// Serve the asset file
|
|
||||||
c.FileFromFS(assetPath, assetsFS)
|
|
||||||
c.Abort()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
c.Next()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
g := engine.Group("/")
|
g := engine.Group("/")
|
||||||
|
|
||||||
s.sub = NewSUBController(
|
s.sub = NewSUBController(
|
||||||
|
|
|
||||||
|
|
@ -87,20 +87,7 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||||
if !a.jsonEnabled {
|
if !a.jsonEnabled {
|
||||||
subJsonURL = ""
|
subJsonURL = ""
|
||||||
}
|
}
|
||||||
// Get base_path from context (set by middleware)
|
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
|
||||||
basePath, exists := c.Get("base_path")
|
|
||||||
if !exists {
|
|
||||||
basePath = "/"
|
|
||||||
}
|
|
||||||
// Add subId to base_path for asset URLs
|
|
||||||
basePathStr := basePath.(string)
|
|
||||||
if basePathStr == "/" {
|
|
||||||
basePathStr = "/" + subId + "/"
|
|
||||||
} else {
|
|
||||||
// Remove trailing slash if exists, add subId, then add trailing slash
|
|
||||||
basePathStr = strings.TrimRight(basePathStr, "/") + "/" + subId + "/"
|
|
||||||
}
|
|
||||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL, basePathStr)
|
|
||||||
c.HTML(200, "subpage.html", gin.H{
|
c.HTML(200, "subpage.html", gin.H{
|
||||||
"title": "subscription.title",
|
"title": "subscription.title",
|
||||||
"cur_ver": config.GetVersion(),
|
"cur_ver": config.GetVersion(),
|
||||||
|
|
|
||||||
|
|
@ -1148,7 +1148,7 @@ func (s *SubService) joinPathWithID(basePath, subId string) string {
|
||||||
|
|
||||||
// BuildPageData parses header and prepares the template view model.
|
// BuildPageData parses header and prepares the template view model.
|
||||||
// BuildPageData constructs page data for rendering the subscription information page.
|
// BuildPageData constructs page data for rendering the subscription information page.
|
||||||
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
|
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
|
||||||
download := common.FormatTraffic(traffic.Down)
|
download := common.FormatTraffic(traffic.Down)
|
||||||
upload := common.FormatTraffic(traffic.Up)
|
upload := common.FormatTraffic(traffic.Up)
|
||||||
total := "∞"
|
total := "∞"
|
||||||
|
|
@ -1167,7 +1167,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
||||||
|
|
||||||
return PageData{
|
return PageData{
|
||||||
Host: hostHeader,
|
Host: hostHeader,
|
||||||
BasePath: basePath,
|
BasePath: "/", // kept as "/"; templates now use context base_path injected from router
|
||||||
SId: subId,
|
SId: subId,
|
||||||
Download: download,
|
Download: download,
|
||||||
Upload: upload,
|
Upload: upload,
|
||||||
|
|
|
||||||
|
|
@ -35,8 +35,8 @@
|
||||||
<a-space direction="vertical" :size="10">
|
<a-space direction="vertical" :size="10">
|
||||||
<a-theme-switch-login></a-theme-switch-login>
|
<a-theme-switch-login></a-theme-switch-login>
|
||||||
<span>{{ i18n "pages.settings.language" }}</span>
|
<span>{{ i18n "pages.settings.language" }}</span>
|
||||||
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
<a-select ref="selectLang" class="w-100" v-model="lang"
|
||||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||||
<span v-text="l.name"></span>
|
<span v-text="l.name"></span>
|
||||||
|
|
@ -68,7 +68,7 @@
|
||||||
</a-input>
|
</a-input>
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-input-password autocomplete="current-password" name="password" v-model.trim="user.password"
|
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||||
placeholder='{{ i18n "password" }}' required>
|
placeholder='{{ i18n "password" }}' required>
|
||||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||||
</a-input-password>
|
</a-input-password>
|
||||||
|
|
@ -81,8 +81,7 @@
|
||||||
</a-form-item>
|
</a-form-item>
|
||||||
<a-form-item>
|
<a-form-item>
|
||||||
<a-row justify="center" class="centered">
|
<a-row justify="center" class="centered">
|
||||||
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
||||||
:style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
|
||||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue