Files
guru-connect/dashboard/src/components/layout/Sidebar.tsx
Mike Swanson 96b4fd7721
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 4m43s
Build and Test / Build Agent (Windows) (push) Successful in 8m48s
Build and Test / Security Audit (push) Successful in 4m38s
Build and Test / Build Summary (push) Has been skipped
feat(dashboard): GuruConnect v2 Users admin view
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>
2026-05-30 14:18:40 -07:00

86 lines
2.4 KiB
TypeScript

import { NavLink } from "react-router-dom";
import type { ComponentType, SVGProps } from "react";
import { useAuth } from "../../auth/AuthContext";
import {
CodesIcon,
MachinesIcon,
SessionsIcon,
UsersIcon,
} from "./icons";
interface NavItem {
to: string;
label: string;
Icon: ComponentType<SVGProps<SVGSVGElement>>;
/** Pass-1 stubs are disabled until their views land in later passes. */
enabled: boolean;
/** Only render for admins (the underlying route is admin-gated). */
adminOnly?: boolean;
}
const NAV: NavItem[] = [
{ to: "/machines", label: "Machines", Icon: MachinesIcon, enabled: true },
{ to: "/sessions", label: "Sessions", Icon: SessionsIcon, enabled: true },
{ to: "/codes", label: "Codes", Icon: CodesIcon, enabled: true },
{
to: "/users",
label: "Users",
Icon: UsersIcon,
enabled: true,
adminOnly: true,
},
];
export function Sidebar() {
const { isAdmin } = useAuth();
// Hide admin-only items from non-admins entirely (the route also gates them,
// and the API is admin-gated server-side — this keeps the UX honest).
const items = NAV.filter((item) => !item.adminOnly || isAdmin);
return (
<aside className="sidebar">
<div className="sidebar__brand">
<span className="sidebar__logo" aria-hidden="true">
GC
</span>
<span className="sidebar__name">
GuruConnect
<small>Operator Console</small>
</span>
</div>
<nav className="sidebar__nav" aria-label="Primary">
<span className="sidebar__section">Operations</span>
{items.map(({ to, label, Icon, enabled }) =>
enabled ? (
<NavLink
key={to}
to={to}
className={({ isActive }) =>
`navlink${isActive ? " navlink--active" : ""}`
}
>
<span className="navlink__icon">
<Icon />
</span>
{label}
</NavLink>
) : (
<span
key={to}
className="navlink navlink--disabled"
aria-disabled="true"
title={`${label} — coming in a later pass`}
>
<span className="navlink__icon">
<Icon />
</span>
{label}
<span className="navlink__soon">Soon</span>
</span>
),
)}
</nav>
</aside>
);
}