diff --git a/src/App.tsx b/src/App.tsx index c99b572..48f2c17 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,8 @@ -import { Routes, Route, Navigate } from 'react-router-dom' +import { Routes, Route, Navigate, useLocation } from 'react-router-dom' import { useAuthStore } from './store/auth' import Layout from './components/layout/Layout' import PageLoader from './components/common/PageLoader' +import { saveReturnUrl } from './utils/token' import Login from './pages/Login' import TelegramCallback from './pages/TelegramCallback' import TelegramRedirect from './pages/TelegramRedirect' @@ -36,13 +37,16 @@ import AdminRemnawave from './pages/AdminRemnawave' function ProtectedRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading } = useAuthStore() + const location = useLocation() if (isLoading) { return } if (!isAuthenticated) { - return + // Сохраняем текущий URL для возврата после авторизации + saveReturnUrl() + return } return {children} @@ -50,13 +54,16 @@ function ProtectedRoute({ children }: { children: React.ReactNode }) { function AdminRoute({ children }: { children: React.ReactNode }) { const { isAuthenticated, isLoading, isAdmin } = useAuthStore() + const location = useLocation() if (isLoading) { return } if (!isAuthenticated) { - return + // Сохраняем текущий URL для возврата после авторизации + saveReturnUrl() + return } if (!isAdmin) { diff --git a/src/pages/Login.tsx b/src/pages/Login.tsx index fd75065..c6e48ca 100644 --- a/src/pages/Login.tsx +++ b/src/pages/Login.tsx @@ -1,9 +1,10 @@ -import { useState, useEffect, useMemo } from 'react' -import { useNavigate } from 'react-router-dom' +import { useState, useEffect, useMemo, useCallback } from 'react' +import { useNavigate, useLocation } from 'react-router-dom' import { useTranslation } from 'react-i18next' import { useQuery } from '@tanstack/react-query' import { useAuthStore } from '../store/auth' import { brandingApi, type BrandingInfo } from '../api/branding' +import { getAndClearReturnUrl } from '../utils/token' import LanguageSwitcher from '../components/LanguageSwitcher' import TelegramLoginButton from '../components/TelegramLoginButton' @@ -46,6 +47,7 @@ const cacheBranding = (data: BrandingInfo) => { export default function Login() { const { t } = useTranslation() const navigate = useNavigate() + const location = useLocation() const { isAuthenticated, loginWithTelegram, loginWithEmail } = useAuthStore() const [activeTab, setActiveTab] = useState<'telegram' | 'email'>('telegram') const [email, setEmail] = useState('') @@ -54,6 +56,22 @@ export default function Login() { const [isLoading, setIsLoading] = useState(false) const [isTelegramWebApp, setIsTelegramWebApp] = useState(false) + // Получаем URL для возврата после авторизации + const getReturnUrl = useCallback(() => { + // Сначала проверяем state от React Router + const stateFrom = (location.state as { from?: string })?.from + if (stateFrom && stateFrom !== '/login') { + return stateFrom + } + // Затем проверяем сохранённый URL в sessionStorage (от safeRedirectToLogin) + const savedUrl = getAndClearReturnUrl() + if (savedUrl && savedUrl !== '/login') { + return savedUrl + } + // По умолчанию на главную + return '/' + }, [location.state]) + // Fetch branding const cachedBranding = useMemo(() => getCachedBranding(), []) @@ -82,9 +100,9 @@ export default function Login() { useEffect(() => { if (isAuthenticated) { - navigate('/') + navigate(getReturnUrl(), { replace: true }) } - }, [isAuthenticated, navigate]) + }, [isAuthenticated, navigate, getReturnUrl]) // Try Telegram WebApp authentication on mount useEffect(() => { @@ -97,7 +115,7 @@ export default function Login() { setIsLoading(true) try { await loginWithTelegram(tg.initData) - navigate('/') + navigate(getReturnUrl(), { replace: true }) } catch (err) { console.error('Telegram auth failed:', err) setError(t('auth.telegramRequired')) @@ -108,7 +126,7 @@ export default function Login() { } tryTelegramAuth() - }, [loginWithTelegram, navigate, t]) + }, [loginWithTelegram, navigate, t, getReturnUrl]) const handleEmailLogin = async (e: React.FormEvent) => { e.preventDefault() @@ -117,7 +135,7 @@ export default function Login() { try { await loginWithEmail(email, password) - navigate('/') + navigate(getReturnUrl(), { replace: true }) } catch (err: unknown) { const error = err as { response?: { data?: { detail?: string } } } setError(error.response?.data?.detail || t('common.error')) diff --git a/src/utils/token.ts b/src/utils/token.ts index 62f32dc..eb0d254 100644 --- a/src/utils/token.ts +++ b/src/utils/token.ts @@ -252,9 +252,40 @@ class TokenRefreshManager { export const tokenRefreshManager = new TokenRefreshManager() +/** + * Ключ для сохранения URL для возврата после логина + */ +const RETURN_URL_KEY = 'auth_return_url' + +/** + * Сохраняет URL для возврата после авторизации + */ +export function saveReturnUrl(): void { + if (typeof window !== 'undefined') { + const currentPath = window.location.pathname + window.location.search + // Не сохраняем /login как return URL + if (currentPath && currentPath !== '/login') { + sessionStorage.setItem(RETURN_URL_KEY, currentPath) + } + } +} + +/** + * Получает и очищает сохранённый URL для возврата + */ +export function getAndClearReturnUrl(): string | null { + if (typeof window !== 'undefined') { + const url = sessionStorage.getItem(RETURN_URL_KEY) + sessionStorage.removeItem(RETURN_URL_KEY) + return url + } + return null +} + /** * Безопасный редирект на страницу логина * Валидирует URL чтобы предотвратить open redirect уязвимость + * Сохраняет текущий URL для возврата после авторизации */ export function safeRedirectToLogin(): void { // Разрешённые пути для редиректа (относительные пути нашего приложения) @@ -262,6 +293,8 @@ export function safeRedirectToLogin(): void { // Проверяем, что мы на том же origin if (typeof window !== 'undefined') { + // Сохраняем текущий URL для возврата после логина + saveReturnUrl() // Используем только относительный путь для безопасности window.location.href = loginPath }