diff --git a/main.go b/main.go
index bc0d038bea2..1d4c4b13e2a 100644
--- a/main.go
+++ b/main.go
@@ -178,10 +178,10 @@ func main() {
store := cookie.NewStore([]byte(common.SessionSecret))
store.Options(sessions.Options{
Path: "/",
- MaxAge: 2592000, // 30 days
+ MaxAge: 2592000,
HttpOnly: true,
- Secure: false,
- SameSite: http.SameSiteStrictMode,
+ Secure: gin.Mode() == gin.ReleaseMode,
+ SameSite: http.SameSiteLaxMode,
})
server.Use(sessions.Sessions("session", store))
diff --git a/router/web-router.go b/router/web-router.go
index 804a357d694..9271521fb2c 100644
--- a/router/web-router.go
+++ b/router/web-router.go
@@ -8,9 +8,7 @@ import (
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/controller"
"github.com/QuantumNous/new-api/middleware"
- "github.com/QuantumNous/new-api/model"
"github.com/gin-contrib/gzip"
- "github.com/gin-contrib/sessions"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
)
@@ -55,42 +53,6 @@ func SetWebRouter(router *gin.Engine, assets ThemeAssets) {
}
func resolveFrontendTheme(c *gin.Context) string {
- themeCookie, err := c.Cookie(common.FrontendThemeCookieName)
- if err == nil {
- theme := common.NormalizeFrontendTheme(themeCookie)
- if theme != "" {
- return theme
- }
- }
-
- session := sessions.Default(c)
- sessionTheme := common.NormalizeFrontendTheme(common.Interface2String(session.Get(common.FrontendThemeSessionKey)))
- if sessionTheme != "" {
- common.SetFrontendThemeCookie(c, sessionTheme)
- return sessionTheme
- }
-
- if sessionID := session.Get("id"); sessionID != nil {
- if userID, ok := sessionID.(int); ok && userID > 0 {
- setting, err := model.GetUserSetting(userID, false)
- if err == nil {
- theme := common.NormalizeFrontendTheme(setting.FrontendTheme)
- if theme != "" {
- session.Set(common.FrontendThemeSessionKey, theme)
- _ = session.Save()
- common.SetFrontendThemeCookie(c, theme)
- return theme
- }
- }
-
- fallbackTheme := common.NormalizeFrontendTheme(common.GetTheme())
- if fallbackTheme != "" {
- session.Set(common.FrontendThemeSessionKey, fallbackTheme)
- _ = session.Save()
- return fallbackTheme
- }
- }
- }
return common.GetTheme()
}
diff --git a/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx b/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx
index 2a8bec2c155..5db0be1542f 100644
--- a/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx
+++ b/web/classic/src/components/settings/personal/cards/PreferencesSettings.jsx
@@ -35,8 +35,7 @@ const languageOptions = [
{ value: 'vi', label: 'Tiếng Việt' },
];
-const FRONTEND_THEME_COOKIE_NAME = 'frontend_theme';
-const FRONTEND_THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
+const ENABLED_CLASSIC_FRONTEND_KEY = 'EnabledClassicFrontend';
const frontendThemeOptions = [
{ value: 'default', label: '新版本 UI' },
@@ -44,44 +43,23 @@ const frontendThemeOptions = [
];
const getFrontendTheme = () => {
- if (typeof document === 'undefined') return 'default';
- const value = `; ${document.cookie}`;
- const parts = value.split(`; ${FRONTEND_THEME_COOKIE_NAME}=`);
- if (parts.length !== 2) return 'default';
- const theme = parts.pop()?.split(';').shift();
- return theme === 'classic' ? 'classic' : 'default';
+ if (typeof localStorage === 'undefined') return 'default';
+ return localStorage.getItem(ENABLED_CLASSIC_FRONTEND_KEY) === 'true' ? 'classic' : 'default';
};
const setFrontendTheme = (theme) => {
- if (typeof document === 'undefined') return;
- document.cookie = `${FRONTEND_THEME_COOKIE_NAME}=${theme}; path=/; max-age=${FRONTEND_THEME_COOKIE_MAX_AGE}`;
+ if (typeof localStorage === 'undefined') return;
+ if (theme === 'classic') {
+ localStorage.setItem(ENABLED_CLASSIC_FRONTEND_KEY, 'true');
+ } else {
+ localStorage.removeItem(ENABLED_CLASSIC_FRONTEND_KEY);
+ }
};
const getFrontendThemeSettingsPath = (theme) => {
return theme === 'classic' ? '/console/personal' : '/profile';
};
-const updateFrontendThemePreference = async (theme, userId) => {
- const response = await fetch('/api/user/self', {
- method: 'PUT',
- credentials: 'same-origin',
- headers: {
- 'Content-Type': 'application/json',
- 'Cache-Control': 'no-store',
- 'New-API-User': String(userId ?? -1),
- },
- body: JSON.stringify({
- frontend_theme: theme,
- }),
- });
-
- const data = await response.json();
- if (!response.ok || !data?.success) {
- throw new Error(data?.message || 'save frontend theme failed');
- }
- return data;
-};
-
const PreferencesSettings = ({ t }) => {
const { i18n } = useTranslation();
const [userState, userDispatch, startThemeNavigation] = useContext(UserContext);
@@ -105,11 +83,6 @@ const PreferencesSettings = ({ t }) => {
i18n.changeLanguage(lang);
}
}
- if (settings.frontend_theme) {
- setCurrentFrontendTheme(
- settings.frontend_theme === 'classic' ? 'classic' : 'default',
- );
- }
} catch (e) {}
}, [userState?.user?.setting, i18n]);
@@ -174,27 +147,6 @@ const PreferencesSettings = ({ t }) => {
const previousTheme = currentFrontendTheme;
try {
- await updateFrontendThemePreference(theme, userState?.user?.id);
-
- let settings = {};
- if (userState?.user?.setting) {
- try {
- settings = JSON.parse(userState.user.setting) || {};
- } catch (e) {
- settings = {};
- }
- }
- settings.frontend_theme = theme;
- const nextUser = {
- ...userState.user,
- setting: JSON.stringify(settings),
- };
- userDispatch({
- type: 'login',
- payload: nextUser,
- });
- localStorage.setItem('user', JSON.stringify(nextUser));
-
setFrontendTheme(theme);
setCurrentFrontendTheme(theme);
showSuccess(t('界面风格已切换,正在跳转'));
diff --git a/web/classic/src/context/User/index.jsx b/web/classic/src/context/User/index.jsx
index 47c056c87cc..bb393bcd46e 100644
--- a/web/classic/src/context/User/index.jsx
+++ b/web/classic/src/context/User/index.jsx
@@ -17,23 +17,11 @@ along with this program. If not, see .
For commercial licensing, please contact support@quantumnous.com
*/
-import React, { useEffect, useRef, useCallback } from 'react';
+import React, { useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { reducer, initialState } from './reducer';
import { normalizeLanguage } from '../../i18n/language';
-const FRONTEND_THEME_COOKIE_NAME = 'frontend_theme';
-const FRONTEND_THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365;
-
-const normalizeFrontendTheme = (value) => {
- return value === 'classic' ? 'classic' : 'default';
-};
-
-const setFrontendTheme = (theme) => {
- if (typeof document === 'undefined') return;
- document.cookie = `${FRONTEND_THEME_COOKIE_NAME}=${theme}; path=/; max-age=${FRONTEND_THEME_COOKIE_MAX_AGE}`;
-};
-
export const UserContext = React.createContext({
state: initialState,
dispatch: () => null,
@@ -43,12 +31,8 @@ export const UserContext = React.createContext({
export const UserProvider = ({ children }) => {
const [state, dispatch] = React.useReducer(reducer, initialState);
const { i18n } = useTranslation();
- const themeRedirectAttemptedRef = useRef(false);
- const themeNavigationPendingRef = useRef(false);
- const startThemeNavigation = useCallback(() => {
- themeNavigationPendingRef.current = true;
- }, []);
+ const startThemeNavigation = React.useCallback(() => {}, []);
useEffect(() => {
if (state.user?.setting) {
@@ -61,16 +45,7 @@ export const UserProvider = ({ children }) => {
if (normalizedLanguage) {
localStorage.setItem('i18nextLng', normalizedLanguage);
}
- if (settings.frontend_theme) {
- const normalizedTheme = normalizeFrontendTheme(settings.frontend_theme);
- setFrontendTheme(normalizedTheme);
- if (normalizedTheme === 'default' && !themeRedirectAttemptedRef.current && !themeNavigationPendingRef.current) {
- themeRedirectAttemptedRef.current = true;
- window.location.replace('/dashboard');
- }
- }
} catch (e) {
- // Ignore parse errors
}
}
}, [state.user?.setting, i18n]);
diff --git a/web/default/src/features/auth/hooks/use-auth-redirect.ts b/web/default/src/features/auth/hooks/use-auth-redirect.ts
index 843d424fef5..d156457b74d 100644
--- a/web/default/src/features/auth/hooks/use-auth-redirect.ts
+++ b/web/default/src/features/auth/hooks/use-auth-redirect.ts
@@ -3,7 +3,7 @@ import i18n from 'i18next'
import { useAuthStore } from '@/stores/auth-store'
import { getSelf } from '@/lib/api'
import {
- normalizeFrontendTheme,
+ getFrontendTheme,
setFrontendTheme,
} from '@/lib/frontend-theme'
import type { User } from '@/features/users/types'
@@ -27,101 +27,56 @@ function getSavedLanguage(user: User): string | undefined {
}
}
-function getSavedFrontendTheme(user: User): 'default' | 'classic' | undefined {
- const userData = user as Record
- if (typeof userData.frontend_theme === 'string') {
- return normalizeFrontendTheme(userData.frontend_theme)
- }
-
- if (typeof userData.setting !== 'string') {
- return undefined
- }
-
- try {
- const setting = JSON.parse(userData.setting) as { frontend_theme?: unknown }
- return typeof setting.frontend_theme === 'string'
- ? normalizeFrontendTheme(setting.frontend_theme)
- : undefined
- } catch {
- return undefined
- }
-}
-
-/**
- * Hook for handling authentication redirects and user data management
- */
export function useAuthRedirect() {
const navigate = useNavigate()
const { auth } = useAuthStore()
- /**
- * Handle successful login
- * @param userData - Optional user data from login response
- * @param redirectTo - Redirect path after login
- */
const handleLoginSuccess = async (
userData?: { id?: number } | null,
redirectTo?: string
) => {
- // Save user ID if available
if (userData?.id) {
saveUserId(userData.id)
}
- // Fetch and set user data
try {
const self = await getSelf()
if (self?.success && self.data) {
const user = self.data as User
auth.setUser(user)
- // Update user ID if not already set
if (user.id) {
saveUserId(user.id)
}
- // Restore saved language preference
const savedLang = getSavedLanguage(user)
if (savedLang && savedLang !== i18n.language) {
i18n.changeLanguage(savedLang)
}
- const savedTheme = getSavedFrontendTheme(user)
- if (savedTheme) {
- setFrontendTheme(savedTheme)
- if (savedTheme === 'classic') {
- window.location.replace('/console')
- return
- }
+ const theme = getFrontendTheme()
+ if (theme === 'classic') {
+ setFrontendTheme('classic')
+ window.location.replace('/console')
+ return
}
}
} catch (error) {
- // eslint-disable-next-line no-console
console.error('Failed to fetch user data:', error)
}
- // Navigate to target page
const targetPath = redirectTo || '/dashboard'
navigate({ to: targetPath, replace: true })
}
- /**
- * Redirect to 2FA page
- */
const redirectTo2FA = () => {
navigate({ to: '/otp', replace: true })
}
- /**
- * Redirect to login page
- */
const redirectToLogin = () => {
navigate({ to: '/sign-in', replace: true })
}
- /**
- * Redirect to register page
- */
const redirectToRegister = () => {
navigate({ to: '/sign-up', replace: true })
}
diff --git a/web/default/src/features/profile/components/frontend-theme-card.tsx b/web/default/src/features/profile/components/frontend-theme-card.tsx
index 19f386d062e..730641bc6aa 100644
--- a/web/default/src/features/profile/components/frontend-theme-card.tsx
+++ b/web/default/src/features/profile/components/frontend-theme-card.tsx
@@ -2,7 +2,6 @@ import { useState } from 'react'
import { MonitorSmartphone } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
-import { useAuthStore } from '@/stores/auth-store'
import {
Select,
SelectContent,
@@ -18,8 +17,6 @@ import {
setFrontendTheme,
type FrontendTheme,
} from '@/lib/frontend-theme'
-import { updateFrontendTheme } from '../api'
-import { parseUserSettings } from '../lib'
const FRONTEND_THEME_OPTIONS: Array<{
value: FrontendTheme
@@ -31,7 +28,6 @@ const FRONTEND_THEME_OPTIONS: Array<{
export function FrontendThemeCard() {
const { t } = useTranslation()
- const { auth } = useAuthStore()
const [currentTheme, setCurrentTheme] = useState(
getFrontendTheme()
)
@@ -43,35 +39,13 @@ export function FrontendThemeCard() {
setSwitching(true)
try {
- const existingSetting =
- typeof auth.user?.setting === 'string'
- ? parseUserSettings(auth.user.setting)
- : (auth.user?.setting ?? {})
- const response = await updateFrontendTheme(nextTheme)
-
- if (!response.success) {
- throw new Error(response.message || t('Failed to update settings'))
- }
-
setFrontendTheme(nextTheme)
setCurrentTheme(nextTheme)
- if (auth.user) {
- useAuthStore.getState().auth.setUser({
- ...auth.user,
- setting: JSON.stringify({
- ...existingSetting,
- frontend_theme: nextTheme,
- }),
- })
- }
-
toast.success(t('Interface style updated. Redirecting...'))
window.setTimeout(() => {
window.location.assign(getFrontendThemeSettingsPath(nextTheme))
}, 300)
- } catch {
- toast.error(t('Failed to update settings'))
} finally {
setSwitching(false)
}
diff --git a/web/default/src/lib/frontend-theme.ts b/web/default/src/lib/frontend-theme.ts
index 1a39f007a78..3c76e4dffbe 100644
--- a/web/default/src/lib/frontend-theme.ts
+++ b/web/default/src/lib/frontend-theme.ts
@@ -1,7 +1,4 @@
-import { getCookie, setCookie } from '@/lib/cookies'
-
-export const FRONTEND_THEME_COOKIE_NAME = 'frontend_theme'
-export const FRONTEND_THEME_COOKIE_MAX_AGE = 60 * 60 * 24 * 365
+export const ENABLED_CLASSIC_FRONTEND_KEY = 'EnabledClassicFrontend'
export type FrontendTheme = 'default' | 'classic'
@@ -15,11 +12,16 @@ export function normalizeFrontendTheme(
}
export function getFrontendTheme(): FrontendTheme {
- return normalizeFrontendTheme(getCookie(FRONTEND_THEME_COOKIE_NAME))
+ const enabledClassic = localStorage.getItem(ENABLED_CLASSIC_FRONTEND_KEY)
+ return enabledClassic === 'true' ? 'classic' : 'default'
}
export function setFrontendTheme(theme: FrontendTheme): void {
- setCookie(FRONTEND_THEME_COOKIE_NAME, theme, FRONTEND_THEME_COOKIE_MAX_AGE)
+ if (theme === 'classic') {
+ localStorage.setItem(ENABLED_CLASSIC_FRONTEND_KEY, 'true')
+ } else {
+ localStorage.removeItem(ENABLED_CLASSIC_FRONTEND_KEY)
+ }
}
export function getFrontendThemeSettingsPath(theme: FrontendTheme): string {
diff --git a/web/default/src/routes/_authenticated/route.tsx b/web/default/src/routes/_authenticated/route.tsx
index 12dfe6d44a4..c228760ca2f 100644
--- a/web/default/src/routes/_authenticated/route.tsx
+++ b/web/default/src/routes/_authenticated/route.tsx
@@ -1,17 +1,15 @@
import { createFileRoute, redirect } from '@tanstack/react-router'
import { useAuthStore } from '@/stores/auth-store'
import { getSelf } from '@/lib/api'
-import { normalizeFrontendTheme, setFrontendTheme } from '@/lib/frontend-theme'
+import { getFrontendTheme } from '@/lib/frontend-theme'
import { AuthenticatedLayout } from '@/components/layout'
-// 内存中的验证标记,避免同一会话中重复验证
let sessionVerified = false
export const Route = createFileRoute('/_authenticated')({
beforeLoad: async ({ location }) => {
const { auth } = useAuthStore.getState()
- // 如果本地没有用户信息,直接跳转登录页
if (!auth.user) {
throw redirect({
to: '/sign-in',
@@ -19,31 +17,12 @@ export const Route = createFileRoute('/_authenticated')({
})
}
- // 本地有用户信息,但需要验证 session 是否有效(每个会话只验证一次)
if (!sessionVerified) {
const res = await getSelf().catch(() => null)
if (res?.success && res.data) {
- // 验证成功,更新用户信息(可能有变化)
auth.setUser(res.data)
- const setting = res.data?.setting
- if (typeof setting === 'string') {
- try {
- const parsed = JSON.parse(setting) as { frontend_theme?: string }
- if (parsed.frontend_theme) {
- const normalizedTheme = normalizeFrontendTheme(parsed.frontend_theme)
- setFrontendTheme(normalizedTheme)
- if (normalizedTheme === 'classic') {
- window.location.replace('/console')
- return
- }
- }
- } catch {
- /* empty */
- }
- }
sessionVerified = true
} else {
- // 验证失败或 API 调用失败,清除本地缓存并跳转登录页
auth.reset()
throw redirect({
to: '/sign-in',
@@ -51,6 +30,12 @@ export const Route = createFileRoute('/_authenticated')({
})
}
}
+
+ const theme = getFrontendTheme()
+ if (theme === 'classic') {
+ window.location.replace('/console')
+ return
+ }
},
component: AuthenticatedLayout,
})