Verified catalogue of mobiles and laptops sold in India, collected from real retail listings (FastAPI backend, React frontend, Postgres/pgvector). - REST API under /api/elec (read-only catalogue; admin endpoints need login) - MCP server (FastMCP) at /mcp/ with list_categories, search_products, get_product and price_history tools - Real ratings and reviews read from product pages and search results - Production Dockerfile (requirements-api.txt, no PyTorch) and .env.production.example; remote database only via an explicit ELEC_ALLOW_REMOTE_DB host/name allowlist - docs/API.md: endpoint and MCP reference with live examples Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
99 lines
3.4 KiB
JavaScript
99 lines
3.4 KiB
JavaScript
import React, { Suspense, lazy } from 'react';
|
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
|
import { AuthProvider } from './context/AuthContext';
|
|
import { useAuth } from './context/useAuth';
|
|
import { ErrorBoundary } from './components/ErrorBoundary';
|
|
import { LoginPage } from './pages/LoginPage';
|
|
|
|
/*
|
|
* Every page except the login screen is loaded on demand.
|
|
*
|
|
* Imported eagerly, the whole app landed in one ~790kB bundle, and every
|
|
* visitor downloaded all of it before they could reach the login form.
|
|
* Splitting per route means each page's code arrives when it is first opened.
|
|
* (The charting library that dominated that bundle went away with the
|
|
* analytics pages, but the split is still worth keeping.)
|
|
*
|
|
* LoginPage stays eager: it is the first thing an unauthenticated visitor
|
|
* sees, so deferring it would only add a round trip before the form appears.
|
|
*/
|
|
const HomePage = lazy(() => import('./pages/HomePage').then((m) => ({ default: m.HomePage })));
|
|
const AdminPage = lazy(() => import('./pages/AdminPage').then((m) => ({ default: m.AdminPage })));
|
|
|
|
function PageFallback({ label }) {
|
|
return (
|
|
<div className="min-h-screen w-full bg-slate-950 text-slate-400 flex items-center justify-center text-sm">
|
|
{label}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function ProtectedRoute({ children, allowedRoles }) {
|
|
const { user, role, restoring } = useAuth();
|
|
|
|
// A token restored from sessionStorage is still being checked against
|
|
// /api/auth/me. Redirecting to /login now would bounce a valid session out
|
|
// on every refresh; rendering now would flash a dashboard about to be torn
|
|
// down if the token turns out to be expired.
|
|
if (restoring) {
|
|
return <PageFallback label="Restoring session..." />;
|
|
}
|
|
|
|
if (!user) {
|
|
return <Navigate to="/login" replace />;
|
|
}
|
|
|
|
const effectiveRole = role || (user.role === 'store' ? 'user' : user.role);
|
|
|
|
if (allowedRoles && !allowedRoles.includes(effectiveRole)) {
|
|
// Every remaining route is admin-only, so there is nowhere else in the app
|
|
// to send a non-admin. This can still be reached by a token minted before
|
|
// the `user` account was disabled - those stay valid for their 12h TTL -
|
|
// so it has to terminate somewhere public rather than redirect in a loop.
|
|
return <Navigate to="/login" replace />;
|
|
}
|
|
|
|
return children;
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<ErrorBoundary>
|
|
<AuthProvider>
|
|
<BrowserRouter>
|
|
{/* Covers the network fetch for a lazily-loaded page chunk. */}
|
|
<Suspense fallback={<PageFallback label="Loading..." />}>
|
|
<Routes>
|
|
{/* Public Login Route */}
|
|
<Route path="/login" element={<LoginPage />} />
|
|
|
|
{/* Catalog Route - RESTRICTED TO ADMIN ONLY */}
|
|
<Route
|
|
path="/"
|
|
element={
|
|
<ProtectedRoute allowedRoles={['admin']}>
|
|
<HomePage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
|
|
{/* Admin Panel Route - RESTRICTED TO ADMIN ONLY */}
|
|
<Route
|
|
path="/admin"
|
|
element={
|
|
<ProtectedRoute allowedRoles={['admin']}>
|
|
<AdminPage />
|
|
</ProtectedRoute>
|
|
}
|
|
/>
|
|
|
|
{/* Catch-all redirect */}
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
</Routes>
|
|
</Suspense>
|
|
</BrowserRouter>
|
|
</AuthProvider>
|
|
</ErrorBoundary>
|
|
);
|
|
}
|