mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-10-27 02:24:40 +00:00
Compare commits
10 commits
ea849d6305
...
1fdc6c80ef
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fdc6c80ef | ||
|
|
89def9aee6 | ||
|
|
3b262cf180 | ||
|
|
b2b0024648 | ||
|
|
5822758b7c | ||
|
|
49430b3991 | ||
|
|
104526aab2 | ||
|
|
a0c07241c0 | ||
|
|
adf3242602 | ||
|
|
3f62592e4b |
12 changed files with 134 additions and 71 deletions
82
.github/workflows/docker.yml
vendored
82
.github/workflows/docker.yml
vendored
|
|
@ -1,7 +1,9 @@
|
||||||
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:
|
||||||
|
|
@ -13,48 +15,48 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
with:
|
with:
|
||||||
submodules: true
|
submodules: true
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
hsanaeii/3x-ui
|
|
||||||
ghcr.io/mhsanaei/3x-ui
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=tag
|
|
||||||
type=pep440,pattern={{version}}
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Docker meta
|
||||||
uses: docker/setup-qemu-action@v3
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
hsanaeii/3x-ui
|
||||||
|
ghcr.io/mhsanaei/3x-ui
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up QEMU
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
with:
|
|
||||||
install: true
|
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
- name: Set up Docker Buildx
|
||||||
uses: docker/login-action@v3
|
uses: docker/setup-buildx-action@v3
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
install: true
|
||||||
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GHCR
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
username: ${{ github.repository_owner }}
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Login to GHCR
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
context: .
|
registry: ghcr.io
|
||||||
push: true
|
username: ${{ github.actor }}
|
||||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
push: true
|
||||||
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
2.8.3
|
2.8.4
|
||||||
2
go.mod
2
go.mod
|
|
@ -96,7 +96,7 @@ require (
|
||||||
golang.org/x/tools v0.37.0 // indirect
|
golang.org/x/tools v0.37.0 // indirect
|
||||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect
|
||||||
google.golang.org/protobuf v1.36.9 // indirect
|
google.golang.org/protobuf v1.36.9 // indirect
|
||||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||||
lukechampine.com/blake3 v1.4.1 // indirect
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
|
|
|
||||||
2
go.sum
2
go.sum
|
|
@ -236,6 +236,8 @@ gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||||
|
|
|
||||||
50
sub/sub.go
50
sub/sub.go
|
|
@ -98,8 +98,14 @@ 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", LinksPath)
|
c.Set("base_path", basePath)
|
||||||
})
|
})
|
||||||
|
|
||||||
Encrypt, err := s.settingService.GetSubEncrypt()
|
Encrypt, err := s.settingService.GetSubEncrypt()
|
||||||
|
|
@ -179,22 +185,48 @@ 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 {
|
||||||
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
assetsFS = 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 {
|
||||||
engine.StaticFS("/assets", http.FS(subFS))
|
assetsFS = 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,7 +87,20 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||||
if !a.jsonEnabled {
|
if !a.jsonEnabled {
|
||||||
subJsonURL = ""
|
subJsonURL = ""
|
||||||
}
|
}
|
||||||
page := a.subService.BuildPageData(subId, hostHeader, traffic, lastOnline, subs, subURL, subJsonURL)
|
// Get base_path from context (set by middleware)
|
||||||
|
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(),
|
||||||
|
|
|
||||||
|
|
@ -1169,7 +1169,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) PageData {
|
func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray.ClientTraffic, lastOnline int64, subs []string, subURL, subJsonURL string, basePath string) PageData {
|
||||||
download := common.FormatTraffic(traffic.Down)
|
download := common.FormatTraffic(traffic.Down)
|
||||||
upload := common.FormatTraffic(traffic.Up)
|
upload := common.FormatTraffic(traffic.Up)
|
||||||
total := "∞"
|
total := "∞"
|
||||||
|
|
@ -1188,7 +1188,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
|
||||||
|
|
||||||
return PageData{
|
return PageData{
|
||||||
Host: hostHeader,
|
Host: hostHeader,
|
||||||
BasePath: "/", // kept as "/"; templates now use context base_path injected from router
|
BasePath: basePath,
|
||||||
SId: subId,
|
SId: subId,
|
||||||
Download: download,
|
Download: download,
|
||||||
Upload: upload,
|
Upload: upload,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
"github.com/mhsanaei/3x-ui/v2/web/service"
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
||||||
|
"github.com/mhsanaei/3x-ui/v2/web/session"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
@ -21,11 +24,21 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// checkAPIAuth is a middleware that returns 404 for unauthenticated API requests
|
||||||
|
// to hide the existence of API endpoints from unauthorized users
|
||||||
|
func (a *APIController) checkAPIAuth(c *gin.Context) {
|
||||||
|
if !session.IsLogin(c) {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
// initRouter sets up the API routes for inbounds, server, and other endpoints.
|
// initRouter sets up the API routes for inbounds, server, and other endpoints.
|
||||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||||
// Main API group
|
// Main API group
|
||||||
api := g.Group("/panel/api")
|
api := g.Group("/panel/api")
|
||||||
api.Use(a.checkLogin)
|
api.Use(a.checkAPIAuth)
|
||||||
|
|
||||||
// Inbounds API
|
// Inbounds API
|
||||||
inbounds := api.Group("/inbounds")
|
inbounds := api.Group("/inbounds")
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,9 @@ func NewIndexController(g *gin.RouterGroup) *IndexController {
|
||||||
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
// initRouter sets up the routes for index, login, logout, and two-factor authentication.
|
||||||
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
func (a *IndexController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/", a.index)
|
g.GET("/", a.index)
|
||||||
g.POST("/login", a.login)
|
|
||||||
g.GET("/logout", a.logout)
|
g.GET("/logout", a.logout)
|
||||||
|
|
||||||
|
g.POST("/login", a.login)
|
||||||
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
g.POST("/getTwoFactorEnable", a.getTwoFactorEnable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import (
|
||||||
type XUIController struct {
|
type XUIController struct {
|
||||||
BaseController
|
BaseController
|
||||||
|
|
||||||
inboundController *InboundController
|
|
||||||
serverController *ServerController
|
|
||||||
settingController *SettingController
|
settingController *SettingController
|
||||||
xraySettingController *XraySettingController
|
xraySettingController *XraySettingController
|
||||||
}
|
}
|
||||||
|
|
@ -32,8 +30,6 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||||
g.GET("/settings", a.settings)
|
g.GET("/settings", a.settings)
|
||||||
g.GET("/xray", a.xraySettings)
|
g.GET("/xray", a.xraySettings)
|
||||||
|
|
||||||
a.inboundController = NewInboundController(g)
|
|
||||||
a.serverController = NewServerController(g)
|
|
||||||
a.settingController = NewSettingController(g)
|
a.settingController = NewSettingController(g)
|
||||||
a.xraySettingController = NewXraySettingController(g)
|
a.xraySettingController = NewXraySettingController(g)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@
|
||||||
{{ template "page/body_start" .}}
|
{{ template "page/body_start" .}}
|
||||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
||||||
<transition name="list" appear>
|
<transition name="list" appear>
|
||||||
<a-layout-content class="under min-h-0">
|
<a-layout-content class="under min-h-0">
|
||||||
<div class="waves-header">
|
<div class="waves-header">
|
||||||
<div class="waves-inner-header"></div>
|
<div class="waves-inner-header"></div>
|
||||||
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
|
@ -20,7 +20,7 @@
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
|
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-y-auto overflow-x-hidden">
|
||||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
||||||
<template v-if="!loadingStates.fetched">
|
<template v-if="!loadingStates.fetched">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
|
|
@ -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"
|
<a-select ref="selectLang" class="w-100" v-model="lang" @change="LanguageManager.setLanguage(lang)"
|
||||||
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
: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="password" name="password" v-model.trim="user.password"
|
<a-input-password autocomplete="current-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,7 +81,8 @@
|
||||||
</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" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem"
|
||||||
|
: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" }}' ]]
|
||||||
|
|
|
||||||
13
web/web.go
13
web/web.go
|
|
@ -95,10 +95,9 @@ type Server struct {
|
||||||
httpServer *http.Server
|
httpServer *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
|
|
||||||
index *controller.IndexController
|
index *controller.IndexController
|
||||||
server *controller.ServerController
|
panel *controller.XUIController
|
||||||
panel *controller.XUIController
|
api *controller.APIController
|
||||||
api *controller.APIController
|
|
||||||
|
|
||||||
xrayService service.XrayService
|
xrayService service.XrayService
|
||||||
settingService service.SettingService
|
settingService service.SettingService
|
||||||
|
|
@ -264,10 +263,14 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
g := engine.Group(basePath)
|
g := engine.Group(basePath)
|
||||||
|
|
||||||
s.index = controller.NewIndexController(g)
|
s.index = controller.NewIndexController(g)
|
||||||
s.server = controller.NewMultiServerController(g)
|
|
||||||
s.panel = controller.NewXUIController(g)
|
s.panel = controller.NewXUIController(g)
|
||||||
s.api = controller.NewAPIController(g)
|
s.api = controller.NewAPIController(g)
|
||||||
|
|
||||||
|
// Add a catch-all route to handle undefined paths and return 404
|
||||||
|
engine.NoRoute(func(c *gin.Context) {
|
||||||
|
c.AbortWithStatus(http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
return engine, nil
|
return engine, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue