fix(web): permission-degrade the app shell for merchant-only users
A user whose role has only validation:create (the bar/lavazh validator) made the shell misbehave: useLiveFeed() connected /api/ws unconditionally, the server's report:read guard 403'd the upgrade, and the capped-backoff reconnect hammered it forever — a 403 in the server log every few seconds. Gate the socket on report:read (mirrors routes/ws.ts WATCH_PERMISSION) and render StatusDot / ShiftButton / DeviceFooter only with their backing permissions (report:read / shift:read / device:read), so a merchant's shell is just the nav + their /validate screen, with zero doomed requests. Claude-Session: https://claude.ai/code/session_01YYkpEsLmoQPaize5ec3oUm
This commit is contained in:
+14
-6
@@ -448,11 +448,17 @@ function ConfirmFigure({ label, value, bold, sub }: { label: string; value: stri
|
||||
function RootLayout() {
|
||||
const { user, setUser } = rootRoute.useRouteContext();
|
||||
const { t } = useTranslation();
|
||||
// One app-wide WebSocket for the live feed (booth + any live widget).
|
||||
useLiveFeed();
|
||||
// Nav is gated by PERMISSION, not role — a tab shows iff the user's role grants
|
||||
// the permission its screen needs (the route guards enforce the same server-side).
|
||||
const show = (perm: Permission) => can(user, perm);
|
||||
// One app-wide WebSocket for the live feed (booth + any live widget) — but ONLY
|
||||
// for roles the server would accept (routes/ws.ts gates on report:read). A
|
||||
// merchant validator must not even attempt it: the 403'd upgrade would reconnect
|
||||
// on backoff forever and spam the server log. Same rule for the widgets that feed
|
||||
// off it (StatusDot) or make their own gated calls (ShiftButton → shift:read,
|
||||
// DeviceFooter → device:read).
|
||||
const canWatch = show("report:read");
|
||||
useLiveFeed(canWatch);
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col bg-term-bg text-term-text">
|
||||
@@ -489,11 +495,11 @@ function RootLayout() {
|
||||
show("shift:read")) && <NavLink to="/setup" label={t("nav.setup")} />}
|
||||
</nav>
|
||||
<div className="ml-auto flex items-center gap-3">
|
||||
{user && <ShiftButton />}
|
||||
{user && show("shift:read") && <ShiftButton />}
|
||||
{user && <LanguageToggle user={user} setUser={setUser} />}
|
||||
{user && <ThemeToggle user={user} setUser={setUser} />}
|
||||
{user && <FontScaleToggle user={user} setUser={setUser} />}
|
||||
<StatusDot />
|
||||
{canWatch && <StatusDot />}
|
||||
{user && (
|
||||
<Link
|
||||
to="/profile"
|
||||
@@ -518,8 +524,10 @@ function RootLayout() {
|
||||
<main className="min-h-0 flex-1 overflow-auto p-3">
|
||||
<Outlet />
|
||||
</main>
|
||||
{/* Fixed device-status footer — relays, readers, cameras, printers. */}
|
||||
{user && <DeviceFooter />}
|
||||
{/* Fixed device-status footer — relays, readers, cameras, printers. Its REST
|
||||
seed needs device:read (and its live updates ride the report:read WS), so
|
||||
it's hidden for roles without device visibility (e.g. merchant validators). */}
|
||||
{user && show("device:read") && <DeviceFooter />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user