Защита маршрутов — полный код¶
ProtectedRoute¶
Файл: frontend/src/routes/ProtectedRoute.tsx
import { Navigate, useLocation } from 'react-router-dom';
import { useAuthStore } from '../stores/auth';
export default function ProtectedRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const location = useLocation();
// Если пользователь не авторизован — перенаправляем на страницу входа,
// сохраняя текущий путь для обратного редиректа после логина
if (!isAuthenticated) {
const redirect = encodeURIComponent(location.pathname + location.search);
return <Navigate to={`/login?redirect=${redirect}`} replace />;
}
return <>{children}</>;
}
Обёртка в App.tsx¶
Файл: frontend/src/App.tsx
// Вспомогательный компонент-обёртка для краткости
function Protected({ children }: { children: React.ReactNode }) {
return <ProtectedRoute>{children}</ProtectedRoute>;
}
export default function App() {
const { needRefresh, applyUpdate } = useServiceWorker();
return (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
{needRefresh && <UpdateAvailableToast onUpdate={applyUpdate} />}
<Routes>
{/* Защищённые маршруты с общим layout (BottomNav) */}
<Route element={<Protected><OfflineErrorBoundary><AppLayout /></OfflineErrorBoundary></Protected>}>
<Route path="/" element={<HomePage />} />
<Route path="/feed" element={<FeedPage />} />
<Route path="/wardrobe" element={<WardrobePage />} />
<Route path="/wardrobe/:itemId" element={<WardrobeItemPage />} />
<Route path="/add" element={<AddItemSourcePage />} />
<Route path="/add/details" element={<AddItemDetailsPage />} />
<Route path="/outfits" element={<OutfitsPage />} />
<Route path="/outfits/build" element={<OutfitBuilderPage />} />
<Route path="/outfits/build/select" element={<ItemSelectPage />} />
<Route path="/outfits/:outfitId" element={<OutfitDetailPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/profile/settings" element={<SettingsPage />} />
<Route path="/share/:outfitId" element={<SharePage />} />
</Route>
{/* Публичные маршруты без layout */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
<Route path="/share/view/:token" element={<ShareRecipientPage />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
);
}
AppLayout¶
Файл: frontend/src/components/layout/AppLayout.tsx
import { Outlet } from 'react-router-dom';
import BottomNav from './BottomNav';
import OfflineBanner from '../ui/OfflineBanner';
import s from './AppLayout.module.css';
export default function AppLayout() {
return (
<div className={s.layout}>
<OfflineBanner />
<main className={s.content} role="main">
<Outlet />
</main>
<BottomNav />
</div>
);
}
Логика редиректов¶
| Ситуация | Поведение |
|---|---|
| Неавторизованный → защищённый маршрут | Редирект на /login?redirect=<путь> |
| Успешный вход (LoginPage) | navigate(decodeURIComponent(redirectTarget), { replace: true }) |
| Успешная регистрация | Аналогичный редирект на redirectTarget |
| ShareRecipientPage → claim без авторизации | Редирект на /login?redirect=/share/view/:token |
Параметр replace: true используется во всех редиректах для предотвращения зацикливания при нажатии кнопки «Назад» в браузере.