Repository navigation
feat(roles): role password, permission flow, route + speaker gating (#469) - #746
Conversation
There was a problem hiding this comment.
Code Review — Needs Changes
Automated review (daily-backlog-pr Phase 3). The Roles feature is well structured — authorizedFetch is applied centrally in src/api/api.ts, state changes go through a typed reducer action, there are no dependency changes, and the new store/hook/guard logic has tests. No Blocking items, but a cluster of auth-flow/secret-storage Warnings (the 3+ rule) makes this Needs Changes:
src/App.tsx:243-248— Persisted role is rehydrated in auseEffect, soroleAuthisnullon first render. WithREQUIRE_AUTH=true, reloading or deep-linking to/createor/managemakesRequireFeatureredirect to/before the role is restored, so valid role holders lose their page. Seed the reducer's initial state fromgetRoleAuth(), or gate the routes until rehydration completes.src/api/role-auth-store.ts:68-76— The raw role password is persisted in plaintext inlocalStorageand sent asBearer <password>(readable by any XSS). At minimum record the trade-off;sessionStoragewould narrow exposure. Also, a later-invalid stored password is never cleared — a 401 on any call leaves the user "signed in" with every request failing until they clear it by hand. Clear stored auth on 401.src/api/api.ts:436-460—requestPermissionshand-rolls error handling instead of going throughhandleFetchRequest; if that's to avoid tripping the auth circuit-breaker on a wrong-password 401, say so in a comment (or givehandleFetchRequesta breaker opt-out) rather than duplicating parsing.src/hooks/use-request-permission.ts:37—e as { status?: number }cast; use a typedHttpError/type guard. Add explicit return types to the exporteduseRequestPermission,RolePassword,RequireFeature, anduseInitiateProductionCall.
Suggestions (non-blocking): validate parsed TStoredRoleAuth shape in readFromStorage; add a direct test for use-has-feature.ts and a component test for program-line-join-card.tsx gating.
Please address the Warnings (especially the rehydration redirect and the 401-not-clearing-stored-auth) and push to the same branch. Moving the linked board items (#631–#635) back to Ready.
…469) Implement the Roles feature end to end on the frontend, dormant by default: - #631 Masked role-password field in user settings whose value feeds the permission request. - #632 Permission request flow: POST /api/v1/permissions with the password, store the granted role + feature flags in app state (password kept only for the auth header), classify 401 / 429 / server-error failures. - #633 Route-level enforcement: RequireFeature guard gates /create and /manage on the stored flags via useHasFeature; "no role held" allows unless the REQUIRE_AUTH global flag is set. - #634 Attach Authorization: Bearer <role-password> to all manager API requests via an authorizedFetch wrapper — only when a password is stored. - #635 Prevent joining program lines as audio feed (speaker) without canJoinLine, gated centrally in the join choke point; listening stays allowed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…n errors Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
33bcdc8 to
e843c32
Compare
There was a problem hiding this comment.
Code Review
PR #746, frontend/roles-469, head e843c32 (rebased on origin/main).
Automated review (daily-backlog-pr Phase 3). All four warnings from the previous Needs-Changes review are addressed and nothing is blocking. Evidence: npm run typecheck clean, npm run lint passes with --max-warnings 0, and npm test passes (65 files, 616 tests).
Prior review warnings
- Role restored after first render: fixed.
useInitializeGlobalStateReducerseedsroleAuthfromgetRoleAuth()via the lazyuseReducerinitializer, soRequireFeaturesees the persisted role on first render (require-feature.test.tsxcovers it). - Plaintext password / no clear on 401: fixed.
src/api/role-auth-store.ts:37-44documents the localStorage trade-off;src/api/authorized-fetch.ts:27-33callsclearRoleAuth()on 401 andApp.tsxdispatchesUPDATE_ROLE_AUTH: null. The password is not copied into global state. (See Warning 1 for a side effect.) requestPermissionsbypasseshandleFetchRequest: fixed.src/api/api.ts:430-437now explains a 401 here means a wrong role password, not an expired session, so it must not trip the auth circuit breaker.- Cast / missing return types: fixed.
src/api/http-error.tsadds anHttpErrortype,isHttpErrorguard andcreateHttpError;use-request-permission.tsuses the guard, and the new hooks +useInitiateProductionCallhave explicit return types.
Blocking
None.
Warnings (follow-ups, non-blocking)
src/api/authorized-fetch.ts:29: any 401 wipes the role. Once a role is stored the 401 handler fires for every endpoint; a session-expiry 401 (not a bad role password) would silently drop a valid role. Consider clearing only on role-validation 401s (e.g.permissions), or document that session expiry also drops the role. A "401 from a non-role endpoint" test would pin the behaviour. Most worth addressing before release.src/api/role-auth-store.ts: persistedfeaturesare trusted and never revalidated. Rotated passwords / changed flags aren't reflected until a request 401s; nostorageevent handling for multi-tab sync;readFromStoragedoesn't validaterole ∈ TRoleor boolean features. A startup re-POST to/permissions(or a verify call) plus astoragelistener would fix this.- Client-side gating is not enforcement.
RequireFeature/canJoinLine/disabled checkbox are all bypassable (localStorage is editable). Acceptable only if the manager enforces every flag server-side from the Bearer token — state that the manager is the source of truth and the frontend gates are UX only. - Unused flags.
canDeleteProduction,canManageLines,canRemoveParticipants,canManageIngestsare defined but no UI gates on them; onlycanCreateProduction/canEditProduction/canJoinLineare enforced. If #633/#635 intentionally cover only those three, say so; otherwise those controls stay visible to roles that cannot use them.
Suggestions
authorized-fetch.ts:26:...init?.headersassumes a plain object;new Headers(init?.headers)then.set(...)is more robust.requestPermissionsattaches a staleAuthorizationheader; sending without it is cleaner.use-websocket.ts:38carries no role credential — confirm the WS channel gating is out of scope.use-request-permission.ts:clearPermissiondouble-dispatchesUPDATE_ROLE_AUTH: null(theApp.tsxlistener already does); andsetLoading(false)may run after unmount (low impact in React 19).role-password.tsx:minLength: 1redundant next torequired; failed-attempt error not cleared on typing.- No
package.jsonchanges — no dependency-policy concern. New logic (role-auth-store,authorized-fetch,use-request-permission,RequireFeature, join gate) has happy-dom tests with no timing hacks.
Verdict: LGTM. All four prior warnings are addressed and nothing is blocking. Warnings 1–4 are worth follow-ups, with Warning 1 (a session-expiry 401 wiping the role) the one most worth fixing before release.
Summary
Implements the Roles feature on the frontend (closes five sub-issues of #469). The whole feature is dormant by default: with no role held and
REQUIRE_AUTHoff, the app behaves exactly as before — no Authorization header is ever sent, no route or UI element is gated.type="password") field in user settings (role-password.tsx), shown on the landing-page settings and the calls-page Profile tab. Its value feeds the permission request; when a role is held it shows the granted role and a "Clear role" action.useRequestPermissioncallsPOST /api/v1/permissionswith the password (API.requestPermissions), stores the grantedrole+featuresin global state (UPDATE_ROLE_AUTH) and the password in a framework-agnostic store for the header. Failures are classified: 401 → invalid password, 429 → rate-limited, else generic server error. The permission call deliberately bypasseshandleFetchRequestso a bad-password 401 does not trip the global auth circuit breaker.RequireFeaturegates/create(canCreateProduction) and/manage(canEditProduction) viauseHasFeature. "No role held" resolves to allow unless the globalREQUIRE_AUTHflag (newgetRequireAuth()in runtime-config, read the same way as the manager base URL / AUTH URL) says otherwise; a role that lacks the feature is redirected to the landing page.authorizedFetchwrapper mergesAuthorization: Bearer <role-password>into everyapi.tscall — but only when a password is stored, so with no role it callsfetchwith byte-identical arguments.features.canJoinLineat the singleinitiateProductionCallchoke point (enforced regardless of which UI triggers it); listening is always allowed. The program-line join card also disables the "Audio feed" option when the role lacks the feature.Dormant-by-default design
REQUIRE_AUTHis on.Test plan
All five commands pass. New tests cover:
role-auth-store.test.ts— store set/get/clear, Bearer header,hasFeaturedormant vs. granted vs. REQUIRE_AUTH.api.test.ts—requestPermissions200 / 401 / 429.use-request-permission.test.ts— success stores role+features and dispatches; 401 / 429 messages; clear.require-feature.test.tsx— allow when feature held, block when role lacks it, allow when no role held (dormant), block when no role + REQUIRE_AUTH on.use-initiate-production-call.test.ts— audio-feed join blocked withoutcanJoinLine, listener allowed, audio-feed allowed when no role held.Network and RTC are stubbed per the project's happy-dom /
vi.stubGlobalpatterns.What a human does next
To activate, on the intercom-manager deployment set
ROLE_ADMIN_PASSWORD/ROLE_EDITOR_PASSWORD/ROLE_PARTICIPANT_PASSWORDandREQUIRE_AUTH=true(and optionally setREQUIRE_AUTH=true/VITE_REQUIRE_AUTH=trueon the frontend if you want "no role held" to block restricted routes). Without that configuration the feature stays dormant and the app behaves exactly as before. To test: open user settings, enter a configured role password under Role, Unlock — restricted routes (/create,/manage) and the program-line audio-feed option unlock according to the granted role; clearing the role restores the gated state.Closes #631
Closes #632
Closes #633
Closes #634
Closes #635
🤖 Generated with Claude Code
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com