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>
57 lines
1.9 KiB
TypeScript
57 lines
1.9 KiB
TypeScript
import { Link, Outlet } from "react-router-dom";
|
|
import { Panel } from "../components/ui/Panel";
|
|
import { useAuth } from "./AuthContext";
|
|
|
|
/**
|
|
* Route gate for admin-only sections (the Users plane). Sits inside
|
|
* ProtectedRoute, so the user is already authenticated here — this only checks
|
|
* the admin role.
|
|
*
|
|
* A non-admin who navigates to an admin route sees a calm, explicit
|
|
* access-denied panel (NOT a redirect loop and NOT a 403 toast storm). The
|
|
* server remains the real authority: the underlying /api/users calls are
|
|
* admin-gated server-side, so this is defense-in-depth plus correct UX.
|
|
*/
|
|
export function AdminRoute() {
|
|
const { isAdmin } = useAuth();
|
|
|
|
if (!isAdmin) {
|
|
return (
|
|
<div className="page">
|
|
<div className="denied">
|
|
<Panel>
|
|
<div className="denied__body">
|
|
<span className="denied__badge" aria-hidden="true">
|
|
<svg
|
|
width="22"
|
|
height="22"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
stroke="currentColor"
|
|
strokeWidth="1.8"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
>
|
|
<rect x="3" y="11" width="18" height="11" rx="2" />
|
|
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
|
</svg>
|
|
</span>
|
|
<h1 className="denied__title">Admins only</h1>
|
|
<p className="denied__msg">
|
|
User management is restricted to administrators. Your account
|
|
does not have admin access. If you need it, ask an administrator
|
|
to update your role.
|
|
</p>
|
|
<Link to="/machines" className="btn btn--primary denied__link">
|
|
Back to Machines
|
|
</Link>
|
|
</div>
|
|
</Panel>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return <Outlet />;
|
|
}
|