From 2ea938e23f9cad4724eebb08b44e8327a07e72e7 Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni <66816045+askmy-stack@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:31:08 -0400 Subject: [PATCH 1/2] Add RequireAuth route guard for Meridian dashboard pages. Unauthenticated visitors are redirected to /login; JWT remains in localStorage after login. Co-authored-by: Cursor --- README.md | 5 +++ frontend/src/App.jsx | 25 ++++++++----- frontend/src/components/RequireAuth.jsx | 18 +++++++++ frontend/src/components/RequireAuth.test.jsx | 39 ++++++++++++++++++++ 4 files changed, 77 insertions(+), 10 deletions(-) create mode 100644 frontend/src/components/RequireAuth.jsx create mode 100644 frontend/src/components/RequireAuth.test.jsx diff --git a/README.md b/README.md index 233cc4c..fd63b38 100644 --- a/README.md +++ b/README.md @@ -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`. + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 4ddda6c..c9ce7b2 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -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'; @@ -24,6 +25,10 @@ const queryClient = new QueryClient({ }, }); +function Protected({ children }) { + return {children}; +} + function App() { return ( @@ -31,16 +36,16 @@ function App() { }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> diff --git a/frontend/src/components/RequireAuth.jsx b/frontend/src/components/RequireAuth.jsx new file mode 100644 index 0000000..69c5e5d --- /dev/null +++ b/frontend/src/components/RequireAuth.jsx @@ -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 ; + } + return children; +} + +export function isAuthenticated() { + return Boolean(localStorage.getItem('meridian_access_token')); +} diff --git a/frontend/src/components/RequireAuth.test.jsx b/frontend/src/components/RequireAuth.test.jsx new file mode 100644 index 0000000..44bfcf2 --- /dev/null +++ b/frontend/src/components/RequireAuth.test.jsx @@ -0,0 +1,39 @@ +import { describe, expect, it, beforeEach } from 'vitest'; +import { MemoryRouter, Routes, Route } from 'react-router-dom'; +import { render, screen } from '@testing-library/react'; +import { RequireAuth } from './RequireAuth'; + +function renderWithAuth(initialPath = '/alerts') { + return render( + + + +
Secret alerts
+ + } + /> + Login page} /> +
+
+ ); +} + +describe('RequireAuth', () => { + beforeEach(() => { + localStorage.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(); + }); +}); From f088b45a6f57e502dc1471e6b170b117b23f27aa Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni <66816045+askmy-stack@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:32:46 -0400 Subject: [PATCH 2/2] Fix RequireAuth vitest localStorage stub for jsdom. Co-authored-by: Cursor --- frontend/src/components/RequireAuth.test.jsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/RequireAuth.test.jsx b/frontend/src/components/RequireAuth.test.jsx index 44bfcf2..79eecfc 100644 --- a/frontend/src/components/RequireAuth.test.jsx +++ b/frontend/src/components/RequireAuth.test.jsx @@ -1,8 +1,17 @@ -import { describe, expect, it, beforeEach } from 'vitest'; +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( @@ -23,7 +32,7 @@ function renderWithAuth(initialPath = '/alerts') { describe('RequireAuth', () => { beforeEach(() => { - localStorage.clear(); + store.clear(); }); it('redirects to login when token missing', () => {