fix(panel): make webBasePath work end-to-end in dev and prod

- Vite dev server reads webBasePath from x-ui.db via node:sqlite and
  injects __X_UI_BASE_PATH__ on every HTML serve, mirroring dist.go.
  Single broad proxy regex catches backend routes whether the URL is
  prefixed or not, and the bypass serves login.html for the bare
  basePath URL so post-logout navigation lands on Vite's own page
  instead of the production dist HTML's hashed asset URLs.
- axios.defaults.baseURL is set from __X_UI_BASE_PATH__ at startup so
  HttpUtil calls reach the backend's basePath group instead of 404ing
  on every prefixed install. fetch() for the public CSRF endpoint
  prepends the prefix manually since it doesn't honor axios defaults.
- Logout/redirect responses set Cache-Control: no-store and the index
  handler's logged-in redirect uses an absolute base_path+panel/ URL,
  preventing browsers from replaying a stale cached 307 that bounced
  the user back to /panel/ after logout.
- ClearSession also issues a Path=/ deletion cookie when basePath is
  not "/", so a legacy cookie from an earlier basePath setting can't
  keep IsLogin returning true after logout.
- getPanelUpdateInfo no longer returns a translated error message on
  GitHub fetch failures, so HttpUtil's auto-popup stays quiet on
  offline / blocked environments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
MHSanaei
2026-05-09 21:47:37 +02:00
parent 72d8ebd269
commit 61c84e8223
6 changed files with 163 additions and 136 deletions
+22 -28
View File
@@ -1,10 +1,9 @@
// Package session provides session management utilities for the 3x-ui web panel.
// It handles user authentication state, login sessions, and session storage using Gin sessions.
package session
import (
"encoding/gob"
"net/http"
"time"
"github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger"
@@ -14,24 +13,15 @@ import (
)
const (
loginUserKey = "LOGIN_USER"
// apiAuthUserKey is the gin-context key under which checkAPIAuth
// stashes a fallback user for Bearer-token-authenticated callers.
// Bearer requests don't carry a session cookie, so handlers that
// scope writes by user.Id (e.g. InboundController.addInbound) would
// otherwise nil-deref. Keeping the override in the gin context
// (not the cookie session) means the fallback never leaks into a
// browser request.
apiAuthUserKey = "api_auth_user"
loginUserKey = "LOGIN_USER"
apiAuthUserKey = "api_auth_user"
sessionCookieName = "3x-ui"
)
func init() {
gob.Register(model.User{})
}
// SetLoginUser stores the authenticated user in the session and persists it.
// gin-contrib/sessions does not auto-save; callers that forget Save() leave
// the cookie out of sync with server state — this helper avoids that pitfall.
func SetLoginUser(c *gin.Context, user *model.User) error {
if user == nil {
return nil
@@ -41,10 +31,6 @@ func SetLoginUser(c *gin.Context, user *model.User) error {
return s.Save()
}
// SetAPIAuthUser stashes a fallback user on the gin context for the
// lifetime of a single bearer-authed request. checkAPIAuth calls this
// after a successful token match so downstream handlers that read
// GetLoginUser don't see nil.
func SetAPIAuthUser(c *gin.Context, user *model.User) {
if user == nil {
return
@@ -52,8 +38,6 @@ func SetAPIAuthUser(c *gin.Context, user *model.User) {
c.Set(apiAuthUserKey, user)
}
// GetLoginUser retrieves the authenticated user from the session.
// Returns nil if no user is logged in or if the session data is invalid.
func GetLoginUser(c *gin.Context) *model.User {
if v, ok := c.Get(apiAuthUserKey); ok {
if u, ok2 := v.(*model.User); ok2 {
@@ -67,8 +51,6 @@ func GetLoginUser(c *gin.Context) *model.User {
}
user, ok := obj.(model.User)
if !ok {
// Stale or incompatible session payload — wipe and persist immediately
// so subsequent requests don't keep hitting the same broken cookie.
s.Delete(loginUserKey)
if err := s.Save(); err != nil {
logger.Warning("session: failed to drop stale user payload:", err)
@@ -78,14 +60,10 @@ func GetLoginUser(c *gin.Context) *model.User {
return &user
}
// IsLogin checks if a user is currently authenticated in the session.
func IsLogin(c *gin.Context) bool {
return GetLoginUser(c) != nil
}
// ClearSession invalidates the session and tells the browser to drop the cookie.
// The cookie attributes (Path/HttpOnly/SameSite) must mirror those used when
// the cookie was created or browsers will keep it.
func ClearSession(c *gin.Context) error {
s := sessions.Default(c)
s.Clear()
@@ -93,12 +71,28 @@ func ClearSession(c *gin.Context) error {
if cookiePath == "" {
cookiePath = "/"
}
secure := c.Request.TLS != nil
s.Options(sessions.Options{
Path: cookiePath,
MaxAge: -1,
HttpOnly: true,
Secure: c.Request.TLS != nil,
Secure: secure,
SameSite: http.SameSiteLaxMode,
})
return s.Save()
if err := s.Save(); err != nil {
return err
}
if cookiePath != "/" {
http.SetCookie(c.Writer, &http.Cookie{
Name: sessionCookieName,
Value: "",
Path: "/",
MaxAge: -1,
Expires: time.Unix(0, 0),
HttpOnly: true,
Secure: secure,
SameSite: http.SameSiteLaxMode,
})
}
return nil
}