fix(web): serve panel SPA routes from NoRoute (#5536)

* fix(web): serve panel SPA routes from NoRoute

Return the React shell for authenticated panel document routes that are not explicitly registered in Gin, such as /panel/hosts. Keep API, CSRF, static-file, method, and Accept exclusions so API misses remain 404 and auth semantics stay unchanged.

* fix(web): remove unreachable panel path guard

The panel path is always built by appending /panel, so it can never be empty.
Remove the redundant fallback branch without changing SPA routing behavior.

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* fix(web): allowlist static-asset extensions in SPA fallback

The blanket path.Ext check rejected any panel route whose last segment contained a dot, which would reintroduce the refresh 404 for a future client route carrying a dotted parameter (version, domain, or email-like value). Restrict the static-asset exclusion to a known, case-insensitive extension allowlist and add predicate regression cases.
This commit is contained in:
w3struk
2026-06-25 00:19:12 +05:00
committed by GitHub
parent 2830f97f50
commit ae9bbdf267
4 changed files with 320 additions and 7 deletions
+6 -6
View File
@@ -2,9 +2,9 @@ package controller
import (
"bytes"
"embed"
"encoding/json"
htmlpkg "html"
"io/fs"
"net/http"
"strings"
"time"
@@ -16,10 +16,10 @@ import (
"github.com/mhsanaei/3x-ui/v3/internal/web/session"
)
var distFS embed.FS
var distFS fs.FS
func SetDistFS(fs embed.FS) {
distFS = fs
func SetDistFS(fsys fs.FS) {
distFS = fsys
}
var distPageBuildTime = time.Now()
@@ -30,7 +30,7 @@ var distPageBuildTime = time.Now()
// produced at frontend build time by scripts/build-openapi.mjs and
// embedded into the binary via the dist FS.
func ServeOpenAPISpec(c *gin.Context) {
body, err := distFS.ReadFile("dist/openapi.json")
body, err := fs.ReadFile(distFS, "dist/openapi.json")
if err != nil {
c.JSON(http.StatusNotFound, gin.H{"success": false, "msg": "openapi.json not found"})
return
@@ -72,7 +72,7 @@ func withServerBasePath(spec []byte, basePath string) ([]byte, error) {
}
func serveDistPage(c *gin.Context, name string) {
body, err := distFS.ReadFile("dist/" + name)
body, err := fs.ReadFile(distFS, "dist/"+name)
if err != nil {
c.String(http.StatusInternalServerError, "missing embedded page: %s", name)
return