mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
Implement return URL handling for authentication; save current URL before redirecting to login and retrieve it after successful login.
This commit is contained in:
13
src/App.tsx
13
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 <PageLoader variant="dark" />
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />
|
||||
// Сохраняем текущий URL для возврата после авторизации
|
||||
saveReturnUrl()
|
||||
return <Navigate to="/login" replace state={{ from: location.pathname }} />
|
||||
}
|
||||
|
||||
return <Layout>{children}</Layout>
|
||||
@@ -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 <PageLoader variant="light" />
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />
|
||||
// Сохраняем текущий URL для возврата после авторизации
|
||||
saveReturnUrl()
|
||||
return <Navigate to="/login" replace state={{ from: location.pathname }} />
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
|
||||
@@ -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'))
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user