Electronics Catalog: API, MCP server, frontend and deployment
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>
This commit is contained in:
98
frontend/src/App.jsx
Normal file
98
frontend/src/App.jsx
Normal file
@@ -0,0 +1,98 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user