refactor(panel): rename injected globals + collapse QR modal entries
Rename the SPA globals injected by Go to drop the ad-hoc dunder shape and free up the bare `webBasePath` name (still the DB setting key) from colliding with the JS global it used to share: window.__X_UI_BASE_PATH__ -> window.X_UI_BASE_PATH window.__X_UI_CUR_VER__ -> window.X_UI_CUR_VER Also rework the QR-Code modal to fold every QR (subscription + JSON sub URL, share links, WireGuard config/peer links) into a single a-collapse with one panel per QR. Subscription panels are listed first and open by default; everything else stays collapsed so a multi-link inbound no longer scrolls forever.
This commit is contained in:
+2
-46
@@ -15,41 +15,14 @@ import (
|
||||
"github.com/mhsanaei/3x-ui/v3/web/session"
|
||||
)
|
||||
|
||||
// distFS is filled in once at startup by the web package via SetDistFS.
|
||||
// It holds the Vite-built frontend (the `dist/<page>.html` files) so
|
||||
// the panel's HTML routes can serve them in production.
|
||||
//
|
||||
// We can't `go:embed` the dist directory directly from this package
|
||||
// because embed.FS only accepts paths relative to the source file —
|
||||
// dist/ lives one directory up. The web package owns the embed and
|
||||
// hands the FS to us through this setter.
|
||||
var distFS embed.FS
|
||||
|
||||
// SetDistFS is called once during server bootstrap by the web package
|
||||
// to hand off the embedded `dist/` filesystem.
|
||||
func SetDistFS(fs embed.FS) {
|
||||
distFS = fs
|
||||
}
|
||||
|
||||
// distPageBuildTime is captured at startup so every served HTML page
|
||||
// reports a stable Last-Modified header and the browser's conditional
|
||||
// GETs can hit the 304 path on repeat loads.
|
||||
var distPageBuildTime = time.Now()
|
||||
|
||||
// serveDistPage reads `dist/<name>` from the embedded FS and writes it
|
||||
// to the response. Two transforms run before send:
|
||||
//
|
||||
// 1. `<script>window.__X_UI_BASE_PATH__ = "..."</script>` is injected
|
||||
// just before </head> so the AppSidebar's link generator sees the
|
||||
// right prefix.
|
||||
// 2. Absolute Vite-emitted asset URLs (`/assets/...`) are rewritten
|
||||
// to include the panel's basePath, so installs running under a
|
||||
// custom URL prefix (e.g. `/myprefix/`) load the bundle from
|
||||
// `/myprefix/assets/...` where the static handler actually lives.
|
||||
//
|
||||
// The HTML responses are served with no-cache so a panel update
|
||||
// reaches users on the next reload; the long-hashed JS/CSS files
|
||||
// under /assets/ stay cacheable indefinitely.
|
||||
func serveDistPage(c *gin.Context, name string) {
|
||||
body, err := distFS.ReadFile("dist/" + name)
|
||||
if err != nil {
|
||||
@@ -62,21 +35,11 @@ func serveDistPage(c *gin.Context, name string) {
|
||||
basePath = "/"
|
||||
}
|
||||
|
||||
// Rewrite asset URLs only when basePath isn't the root — for the
|
||||
// default `/` install, Vite's `/assets/...` already resolves
|
||||
// correctly and we save the byte churn.
|
||||
if basePath != "/" {
|
||||
// Vite emits these three attribute shapes for every entry's
|
||||
// JS / CSS / modulepreload reference. Anchoring the search to
|
||||
// the leading attribute name avoids matching unrelated /assets
|
||||
// substrings inside any inlined script.
|
||||
body = bytes.ReplaceAll(body, []byte(`src="/assets/`), []byte(`src="`+basePath+`assets/`))
|
||||
body = bytes.ReplaceAll(body, []byte(`href="/assets/`), []byte(`href="`+basePath+`assets/`))
|
||||
}
|
||||
|
||||
// Escape just enough that a hostile basePath setting can't break
|
||||
// out of the JS string literal. The setting is admin-controlled
|
||||
// but defense-in-depth costs nothing here.
|
||||
jsEscape := strings.NewReplacer(
|
||||
`\`, `\\`,
|
||||
`"`, `\"`,
|
||||
@@ -88,13 +51,6 @@ func serveDistPage(c *gin.Context, name string) {
|
||||
)
|
||||
escapedBase := jsEscape.Replace(basePath)
|
||||
escapedVer := jsEscape.Replace(config.GetVersion())
|
||||
|
||||
// Embed a CSRF token in the served HTML the same way the legacy
|
||||
// templates did via `<meta name="csrf-token">`. Without this the
|
||||
// SPA login page has no way to acquire a token (the existing
|
||||
// /panel/csrf-token endpoint sits behind checkLogin), and POST
|
||||
// /login is rejected by CSRFMiddleware. EnsureCSRFToken creates
|
||||
// a session token on first call even for anonymous visitors.
|
||||
csrfToken, err := session.EnsureCSRFToken(c)
|
||||
if err != nil {
|
||||
logger.Warning("Unable to mint CSRF token for", name+":", err)
|
||||
@@ -102,8 +58,8 @@ func serveDistPage(c *gin.Context, name string) {
|
||||
}
|
||||
csrfMeta := []byte(`<meta name="csrf-token" content="` + htmlpkg.EscapeString(csrfToken) + `">`)
|
||||
|
||||
inject := []byte(`<script>window.__X_UI_BASE_PATH__="` + escapedBase +
|
||||
`";window.__X_UI_CUR_VER__="` + escapedVer + `";</script>`)
|
||||
inject := []byte(`<script>window.X_UI_BASE_PATH="` + escapedBase +
|
||||
`";window.X_UI_CUR_VER="` + escapedVer + `";</script>`)
|
||||
inject = append(inject, csrfMeta...)
|
||||
inject = append(inject, []byte(`</head>`)...)
|
||||
out := bytes.Replace(body, []byte("</head>"), inject, 1)
|
||||
|
||||
Reference in New Issue
Block a user