Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -192,3 +192,8 @@ Training labels: `data/disruption_labels.csv` — 50+ public disruption case stu
**Phase A (real data):** TimescaleDB score history, link confidence, batch rescore — see [docs/REAL_DATA_PHASE_A.md](docs/REAL_DATA_PHASE_A.md).

---

### Frontend auth guard

Protected dashboard routes use a `RequireAuth` wrapper (`frontend/src/components/RequireAuth.jsx`) that redirects to `/login` when no JWT is in `localStorage`.

25 changes: 15 additions & 10 deletions frontend/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { BrowserRouter, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from 'react-query';
import { Layout } from './components/Layout';
import { RequireAuth } from './components/RequireAuth';
import { EntityDrawerProvider } from './context/EntityDrawerContext';
import { Dashboard } from './pages/Dashboard';
import { NetworkView } from './pages/NetworkView';
Expand All @@ -24,23 +25,27 @@ const queryClient = new QueryClient({
},
});

function Protected({ children }) {
return <RequireAuth>{children}</RequireAuth>;
}

function App() {
return (
<QueryClientProvider client={queryClient}>
<EntityDrawerProvider>
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Dashboard />} />
<Route path="network" element={<NetworkView />} />
<Route path="map" element={<RiskMapView />} />
<Route path="timeline" element={<TimelineView />} />
<Route path="sectors" element={<SectorsView />} />
<Route path="suppliers" element={<SuppliersView />} />
<Route path="simulate" element={<SimulationView />} />
<Route path="copilot" element={<CopilotView />} />
<Route path="ops/graph-health" element={<GraphHealthView />} />
<Route path="alerts" element={<AlertsView />} />
<Route index element={<Protected><Dashboard /></Protected>} />
<Route path="network" element={<Protected><NetworkView /></Protected>} />
<Route path="map" element={<Protected><RiskMapView /></Protected>} />
<Route path="timeline" element={<Protected><TimelineView /></Protected>} />
<Route path="sectors" element={<Protected><SectorsView /></Protected>} />
<Route path="suppliers" element={<Protected><SuppliersView /></Protected>} />
<Route path="simulate" element={<Protected><SimulationView /></Protected>} />
<Route path="copilot" element={<Protected><CopilotView /></Protected>} />
<Route path="ops/graph-health" element={<Protected><GraphHealthView /></Protected>} />
<Route path="alerts" element={<Protected><AlertsView /></Protected>} />
<Route path="login" element={<LoginView />} />
</Route>
</Routes>
Expand Down
18 changes: 18 additions & 0 deletions frontend/src/components/RequireAuth.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Navigate, useLocation } from 'react-router-dom';

/**
* Route guard: redirect unauthenticated users to /login.
* JWT is stored in localStorage by api/client.login().
*/
export function RequireAuth({ children }) {
const location = useLocation();
const token = localStorage.getItem('meridian_access_token');
if (!token) {
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
}
return children;
}

export function isAuthenticated() {
return Boolean(localStorage.getItem('meridian_access_token'));
}
48 changes: 48 additions & 0 deletions frontend/src/components/RequireAuth.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { describe, expect, it, beforeEach, vi } from 'vitest';
import { MemoryRouter, Routes, Route } from 'react-router-dom';
import { render, screen } from '@testing-library/react';
import { RequireAuth } from './RequireAuth';

const store = new Map();
const localStorageMock = {
getItem: (k) => (store.has(k) ? store.get(k) : null),
setItem: (k, v) => store.set(k, String(v)),
removeItem: (k) => store.delete(k),
clear: () => store.clear(),
};
vi.stubGlobal('localStorage', localStorageMock);

function renderWithAuth(initialPath = '/alerts') {
return render(
<MemoryRouter initialEntries={[initialPath]}>
<Routes>
<Route
path="/alerts"
element={
<RequireAuth>
<div>Secret alerts</div>
</RequireAuth>
}
/>
<Route path="/login" element={<div>Login page</div>} />
</Routes>
</MemoryRouter>
);
}

describe('RequireAuth', () => {
beforeEach(() => {
store.clear();
});

it('redirects to login when token missing', () => {
renderWithAuth();
expect(screen.getByText('Login page')).toBeTruthy();
});

it('renders children when token present', () => {
localStorage.setItem('meridian_access_token', 'test-jwt');
renderWithAuth();
expect(screen.getByText('Secret alerts')).toBeTruthy();
});
});
Loading