Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The change to http.SameSiteLaxMode is appropriate for resolving the redirect loop issues described. However, I noticed that the Secure attribute is set to false on line 183. For session cookies, it is a security best practice to set Secure: true in production environments to ensure the cookie is only transmitted over HTTPS, preventing session hijacking via man-in-the-middle attacks. Consider making this value conditional based on the environment (e.g., Secure: os.Getenv("GIN_MODE") == "release").

})
server.Use(sessions.Sessions("session", store))

Expand Down
38 changes: 0 additions & 38 deletions router/web-router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,53 +35,31 @@ 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' },
{ value: 'classic', label: '旧版本 UI' },
];

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);
Expand All @@ -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]);

Expand Down Expand Up @@ -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('界面风格已切换,正在跳转'));
Expand Down
29 changes: 2 additions & 27 deletions web/classic/src/context/User/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
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,
Expand All @@ -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) {
Expand All @@ -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]);
Expand Down
57 changes: 6 additions & 51 deletions web/default/src/features/auth/hooks/use-auth-redirect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -27,101 +27,56 @@ function getSavedLanguage(user: User): string | undefined {
}
}

function getSavedFrontendTheme(user: User): 'default' | 'classic' | undefined {
const userData = user as Record<string, unknown>
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 })
}
Expand Down
Loading
Loading