Admin-only user management: list, create, edit role/permissions/status, reset password, and disable/delete, against the v2 users API. - Admin-gated three ways: AdminRoute on /users (calm access-denied panel for non-admins, no redirect loop or data fetch), Sidebar hides the nav item, and every mutation relies on the server AdminUser 403 as the real authority. isAdmin is derived from the server-validated user, not the client token. - Users table: role badge (admin/operator/viewer), permissions summary, enabled/disabled status, created, last-login. Sticky header, skeleton, empty/error states. Self row tagged "You". - Create/edit use the real roles and permission strings (view/control/transfer/manage_users/manage_clients); admin permissions are server-implicit and shown locked. Passwords: typed or Web Crypto generated (rejection-sampled, copy-once reveal), type=password + autoComplete=new-password, cleared from state on open/close/success, never logged/persisted/in-URL; blank on edit means unchanged. - Self-lockout guards: cannot disable, delete, or demote your own admin account (controls disabled + submit-handler checks, matched on the authoritative user id). Server mirrors self-disable/self-delete; the self-demotion guard is client-side (server todo filed). - useUpdateUser sequences user-update then permissions-set; invalidates ["users"] on settled so the table reconciles after a partial failure, with an actionable message if only permissions failed. Passed Code Review (no blockers after fixes) and local gates (tsc/lint/build green). Completes the v2 dashboard view set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
52 lines
2.0 KiB
TypeScript
52 lines
2.0 KiB
TypeScript
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { Navigate, Route, BrowserRouter, Routes } from "react-router-dom";
|
|
import { AdminRoute } from "./auth/AdminRoute";
|
|
import { AuthProvider } from "./auth/AuthProvider";
|
|
import { ProtectedRoute } from "./auth/ProtectedRoute";
|
|
import { AppShell } from "./components/layout/AppShell";
|
|
import { ToastProvider } from "./components/ui/toast";
|
|
import { LoginPage } from "./features/auth/LoginPage";
|
|
import { SupportCodesPage } from "./features/codes/SupportCodesPage";
|
|
import { MachinesPage } from "./features/machines/MachinesPage";
|
|
import { SessionsPage } from "./features/sessions/SessionsPage";
|
|
import { UsersPage } from "./features/users/UsersPage";
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
retry: 1,
|
|
refetchOnWindowFocus: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
export function App() {
|
|
return (
|
|
<QueryClientProvider client={queryClient}>
|
|
<BrowserRouter>
|
|
<ToastProvider>
|
|
<AuthProvider>
|
|
<Routes>
|
|
<Route path="/login" element={<LoginPage />} />
|
|
<Route element={<ProtectedRoute />}>
|
|
<Route element={<AppShell />}>
|
|
<Route path="/machines" element={<MachinesPage />} />
|
|
<Route path="/sessions" element={<SessionsPage />} />
|
|
<Route path="/codes" element={<SupportCodesPage />} />
|
|
{/* Users is admin-only: AdminRoute renders an access-denied
|
|
panel for non-admins instead of the view. */}
|
|
<Route element={<AdminRoute />}>
|
|
<Route path="/users" element={<UsersPage />} />
|
|
</Route>
|
|
<Route path="/" element={<Navigate to="/machines" replace />} />
|
|
</Route>
|
|
</Route>
|
|
<Route path="*" element={<Navigate to="/machines" replace />} />
|
|
</Routes>
|
|
</AuthProvider>
|
|
</ToastProvider>
|
|
</BrowserRouter>
|
|
</QueryClientProvider>
|
|
);
|
|
}
|