API: - PATCH /auth/me — update email and display name - PATCH /auth/me/password — change password (requires current) - GET /auth/me now returns full profile (email, full_name, role) CLI: - python -m src.cli.reset_password --email <email> --password <pw> for recovery when locked out (run via docker exec) Admin UI: - User menu dropdown on the top nav (click username → Account / Sign out) replaces the inline sign-out link - /account page with profile form (email + display name) and change password form (current + new + confirm)
93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
import { useEffect, useState } from 'react';
|
|
import {
|
|
BrowserRouter,
|
|
Navigate,
|
|
Route,
|
|
Routes,
|
|
useLocation,
|
|
} from 'react-router-dom';
|
|
|
|
import Layout from './components/Layout';
|
|
import { trackPageView } from './services/analytics';
|
|
import ProtectedRoute from './components/ProtectedRoute';
|
|
import AccountPage from './pages/AccountPage';
|
|
import ConsentRecordsPage from './pages/ConsentRecordsPage';
|
|
import LoginPage from './pages/LoginPage';
|
|
import SettingsPage from './pages/SettingsPage';
|
|
import SiteDetailPage from './pages/SiteDetailPage';
|
|
import SiteGroupDetailPage from './pages/SiteGroupDetailPage';
|
|
import SitesPage from './pages/SitesPage';
|
|
import { useAuthStore } from './stores/auth';
|
|
import { discoverExtensions, getPages } from './extensions/registry';
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: 1,
|
|
staleTime: 30_000,
|
|
},
|
|
},
|
|
});
|
|
|
|
function AppRoutes() {
|
|
const { loadUser, isAuthenticated } = useAuthStore();
|
|
const location = useLocation();
|
|
const [extensionsReady, setExtensionsReady] = useState(false);
|
|
|
|
useEffect(() => {
|
|
loadUser();
|
|
discoverExtensions().then(() => setExtensionsReady(true));
|
|
}, [loadUser]);
|
|
|
|
useEffect(() => {
|
|
trackPageView(location.pathname);
|
|
}, [location.pathname]);
|
|
|
|
const extensionPages = extensionsReady ? getPages() : [];
|
|
|
|
return (
|
|
<Routes>
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route
|
|
element={
|
|
<ProtectedRoute>
|
|
<Layout />
|
|
</ProtectedRoute>
|
|
}
|
|
>
|
|
<Route path="/sites" element={<SitesPage />} />
|
|
<Route path="/sites/:siteId" element={<SiteDetailPage />} />
|
|
<Route path="/groups/:groupId" element={<SiteGroupDetailPage />} />
|
|
<Route path="/consent" element={<ConsentRecordsPage />} />
|
|
<Route path="/account" element={<AccountPage />} />
|
|
<Route path="/settings" element={<SettingsPage />} />
|
|
{extensionPages
|
|
.filter((p) => p.protected !== false)
|
|
.map((p) => (
|
|
<Route key={p.path} path={p.path} element={<p.component />} />
|
|
))}
|
|
</Route>
|
|
{extensionPages
|
|
.filter((p) => p.protected === false)
|
|
.map((p) => (
|
|
<Route key={p.path} path={p.path} element={<p.component />} />
|
|
))}
|
|
<Route
|
|
path="*"
|
|
element={<Navigate to={isAuthenticated ? '/sites' : '/login'} replace />}
|
|
/>
|
|
</Routes>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<BrowserRouter>
|
|
<AppRoutes />
|
|
</BrowserRouter>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|