2026-05-03 19:51:57 -05:00
|
|
|
|
2026-05-10 00:18:36 -05:00
|
|
|
import { lazy, Suspense, useId } from 'react';
|
2026-05-03 19:51:57 -05:00
|
|
|
import { Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
|
|
|
|
import { useAuth } from '@/hooks/useAuth';
|
|
|
|
|
import Layout from '@/components/layout/Layout';
|
2026-05-04 20:12:57 -05:00
|
|
|
import AppNavigation from '@/components/layout/Sidebar';
|
2026-05-03 19:51:57 -05:00
|
|
|
import { ReleaseNotesDialog } from '@/components/ReleaseNotesDialog';
|
|
|
|
|
import LoginPage from '@/pages/LoginPage';
|
2026-05-09 18:33:02 -05:00
|
|
|
import ErrorBoundary from '@/components/ErrorBoundary';
|
2026-05-09 22:01:19 -05:00
|
|
|
import PageLoader from '@/components/PageLoader';
|
|
|
|
|
|
2026-05-10 03:10:43 -05:00
|
|
|
// TanStack Query
|
|
|
|
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
|
|
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
|
|
|
|
|
|
|
|
|
const queryClient = new QueryClient({
|
|
|
|
|
defaultOptions: {
|
|
|
|
|
queries: {
|
|
|
|
|
staleTime: 1000 * 60 * 2, // 2 minutes
|
|
|
|
|
retry: 1,
|
|
|
|
|
refetchOnWindowFocus: false,
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-09 22:01:19 -05:00
|
|
|
// Lazy-loaded components
|
|
|
|
|
const AdminPage = lazy(() => import('@/pages/AdminPage'));
|
|
|
|
|
const TrackerPage = lazy(() => import('@/pages/TrackerPage'));
|
|
|
|
|
const CalendarPage = lazy(() => import('@/pages/CalendarPage'));
|
|
|
|
|
const SummaryPage = lazy(() => import('@/pages/SummaryPage'));
|
|
|
|
|
const BillsPage = lazy(() => import('@/pages/BillsPage'));
|
|
|
|
|
const CategoriesPage = lazy(() => import('@/pages/CategoriesPage'));
|
|
|
|
|
const SettingsPage = lazy(() => import('@/pages/SettingsPage'));
|
|
|
|
|
const StatusPage = lazy(() => import('@/pages/StatusPage'));
|
|
|
|
|
const AnalyticsPage = lazy(() => import('@/pages/AnalyticsPage'));
|
|
|
|
|
const ReleaseNotesPage = lazy(() => import('@/pages/ReleaseNotesPage'));
|
|
|
|
|
const AboutPage = lazy(() => import('@/pages/AboutPage'));
|
v0.25.0: roadmap redesign, import CSRF fix, AdminDashboard removed
- RoadmapPage: kanban-style priority lanes, shadcn Collapsible/Tabs,
lazy-loaded activity log, admin-only /api/about/roadmap + /dev-log endpoints
- Import CSRF fix: added x-csrf-token header to importAdminBackup,
previewSpreadsheetImport, previewUserDbImport raw fetch() calls
- Removed AdminDashboard.jsx, replaced by RoadmapPage
- Added @radix-ui/react-collapsible + collapsible shadcn component
- Security audit by Private_Hudson: PASS (CSRF fix verified,
admin endpoints gated, path traversal mitigated, XSS safe)
2026-05-11 21:42:36 -05:00
|
|
|
const RoadmapPage = lazy(() => import('@/pages/RoadmapPage'));
|
2026-05-09 22:01:19 -05:00
|
|
|
const DataPage = lazy(() => import('@/pages/DataPage'));
|
|
|
|
|
const ProfilePage = lazy(() => import('@/pages/ProfilePage'));
|
2026-05-14 02:11:54 -05:00
|
|
|
const SnowballPage = lazy(() => import('@/pages/SnowballPage'));
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
function RequireAuth({ children, role }) {
|
|
|
|
|
const { user, singleUserMode } = useAuth();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
|
|
|
|
|
// Loading state
|
|
|
|
|
if (user === undefined) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex items-center justify-center min-h-screen bg-background text-muted-foreground text-sm">
|
|
|
|
|
Loading...
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Single-user mode bypass
|
|
|
|
|
if (singleUserMode && role === 'user') return children;
|
|
|
|
|
|
|
|
|
|
// Not authenticated
|
|
|
|
|
if (!user) {
|
|
|
|
|
return <Navigate to="/login" state={{ from: location }} replace />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 20:40:48 -05:00
|
|
|
const roleAllowed = !role || user.role === role || (role === 'user' && user.role === 'admin');
|
|
|
|
|
|
2026-05-04 23:34:24 -05:00
|
|
|
if (role === 'user' && user.is_default_admin) {
|
|
|
|
|
return <Navigate to="/admin" replace />;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
// Role mismatch
|
2026-05-03 20:40:48 -05:00
|
|
|
if (!roleAllowed) {
|
2026-05-03 19:51:57 -05:00
|
|
|
return <Navigate to={user.role === 'admin' ? '/admin' : '/'} replace />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return children;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-04 20:12:57 -05:00
|
|
|
function AdminShell({ children }) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="min-h-screen bg-[radial-gradient(circle_at_top_left,oklch(var(--primary)/0.10),transparent_34rem),linear-gradient(180deg,oklch(var(--background)),oklch(var(--muted)/0.28))] text-foreground">
|
|
|
|
|
<AppNavigation adminMode />
|
|
|
|
|
<main className="mx-auto max-w-5xl px-4 py-6 sm:px-6 lg:px-8 lg:py-8">
|
|
|
|
|
{children}
|
|
|
|
|
</main>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-03 19:51:57 -05:00
|
|
|
export default function App() {
|
|
|
|
|
const { user } = useAuth();
|
2026-05-10 00:18:36 -05:00
|
|
|
const mainContentId = useId();
|
2026-05-03 19:51:57 -05:00
|
|
|
|
|
|
|
|
return (
|
2026-05-10 03:10:43 -05:00
|
|
|
<QueryClientProvider client={queryClient}>
|
2026-05-03 19:51:57 -05:00
|
|
|
{/* Release notes (only for user role) */}
|
|
|
|
|
{user?.role === 'user' && <ReleaseNotesDialog />}
|
|
|
|
|
|
2026-05-10 00:18:36 -05:00
|
|
|
{/* Skip link for keyboard users */}
|
|
|
|
|
<a
|
|
|
|
|
href={`#${mainContentId}`}
|
|
|
|
|
className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-50 focus:bg-background focus:text-foreground focus:px-4 focus:py-2 focus:rounded-md focus:shadow-lg focus:outline-none"
|
|
|
|
|
>
|
|
|
|
|
Skip to main content
|
|
|
|
|
</a>
|
|
|
|
|
|
|
|
|
|
<main id={mainContentId}>
|
|
|
|
|
<Routes>
|
|
|
|
|
<Route path="/login" element={<ErrorBoundary><LoginPage /></ErrorBoundary>} />
|
|
|
|
|
<Route path="/about" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><AboutPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="/release-notes" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><ReleaseNotesPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
|
|
|
|
|
<Route
|
|
|
|
|
path="/admin"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
2026-05-09 22:01:19 -05:00
|
|
|
<Suspense fallback={<PageLoader />}>
|
2026-05-10 00:18:36 -05:00
|
|
|
<AdminPage />
|
2026-05-09 22:01:19 -05:00
|
|
|
</Suspense>
|
2026-05-10 00:18:36 -05:00
|
|
|
</ErrorBoundary>
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/admin/about"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<AdminShell>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
v0.25.0: roadmap redesign, import CSRF fix, AdminDashboard removed
- RoadmapPage: kanban-style priority lanes, shadcn Collapsible/Tabs,
lazy-loaded activity log, admin-only /api/about/roadmap + /dev-log endpoints
- Import CSRF fix: added x-csrf-token header to importAdminBackup,
previewSpreadsheetImport, previewUserDbImport raw fetch() calls
- Removed AdminDashboard.jsx, replaced by RoadmapPage
- Added @radix-ui/react-collapsible + collapsible shadcn component
- Security audit by Private_Hudson: PASS (CSRF fix verified,
admin endpoints gated, path traversal mitigated, XSS safe)
2026-05-11 21:42:36 -05:00
|
|
|
<AboutPage />
|
2026-05-10 00:18:36 -05:00
|
|
|
</Suspense>
|
|
|
|
|
</AdminShell>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/admin/roadmap"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<AdminShell>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
v0.25.0: roadmap redesign, import CSRF fix, AdminDashboard removed
- RoadmapPage: kanban-style priority lanes, shadcn Collapsible/Tabs,
lazy-loaded activity log, admin-only /api/about/roadmap + /dev-log endpoints
- Import CSRF fix: added x-csrf-token header to importAdminBackup,
previewSpreadsheetImport, previewUserDbImport raw fetch() calls
- Removed AdminDashboard.jsx, replaced by RoadmapPage
- Added @radix-ui/react-collapsible + collapsible shadcn component
- Security audit by Private_Hudson: PASS (CSRF fix verified,
admin endpoints gated, path traversal mitigated, XSS safe)
2026-05-11 21:42:36 -05:00
|
|
|
<RoadmapPage />
|
2026-05-10 00:18:36 -05:00
|
|
|
</Suspense>
|
|
|
|
|
</AdminShell>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/admin/status"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<ErrorBoundary>
|
|
|
|
|
<AdminShell>
|
|
|
|
|
<Suspense fallback={<PageLoader />}>
|
|
|
|
|
<StatusPage />
|
|
|
|
|
</Suspense>
|
|
|
|
|
</AdminShell>
|
|
|
|
|
</ErrorBoundary>
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Route
|
|
|
|
|
path="/status"
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="admin">
|
|
|
|
|
<Navigate to="/admin/status" replace />
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
<Route
|
|
|
|
|
element={
|
|
|
|
|
<RequireAuth role="user">
|
|
|
|
|
<Layout mainContentId={mainContentId} />
|
|
|
|
|
</RequireAuth>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Route index element={<ErrorBoundary><Suspense fallback={<PageLoader />}><TrackerPage mainContentId={mainContentId} /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="calendar" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><CalendarPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="summary" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><SummaryPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="bills" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><BillsPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="categories" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><CategoriesPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="analytics" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><AnalyticsPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="settings" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><SettingsPage /></Suspense></ErrorBoundary>} />
|
2026-05-14 02:11:54 -05:00
|
|
|
<Route path="snowball" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><SnowballPage /></Suspense></ErrorBoundary>} />
|
2026-05-10 00:18:36 -05:00
|
|
|
<Route path="data" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><DataPage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="profile" element={<ErrorBoundary><Suspense fallback={<PageLoader />}><ProfilePage /></Suspense></ErrorBoundary>} />
|
|
|
|
|
<Route path="*" element={<Navigate to="/" replace />} />
|
|
|
|
|
</Route>
|
|
|
|
|
</Routes>
|
|
|
|
|
</main>
|
2026-05-10 03:10:43 -05:00
|
|
|
<ReactQueryDevtools initialIsOpen={false} />
|
|
|
|
|
</QueryClientProvider>
|
2026-05-03 19:51:57 -05:00
|
|
|
);
|
|
|
|
|
}
|