fix(auth): make admin pages reachable — async capability guard + sticky dev role + nav
CI / storybook-a11y (push) Failing after 4m28s
CI / frontend (push) Successful in 1m44s
CI / backend (push) Successful in 1m28s
CI / e2e (push) Successful in 2m49s
CI / codeql (csharp) (push) Failing after 2m8s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 2m6s
CI / storybook-a11y (push) Failing after 4m28s
CI / frontend (push) Successful in 1m44s
CI / backend (push) Successful in 1m28s
CI / e2e (push) Successful in 2m49s
CI / codeql (csharp) (push) Failing after 2m8s
CI / codeql (javascript-typescript) (push) Failing after 1m30s
CI / api-client-drift (push) Successful in 2m6s
The admin pages (/beheer/stamdata, /brief/huisstijl) were unreachable in the browser, for three compounding reasons — all fixed here: - **Guard raced /me.** capabilityGuard read can() synchronously while /me was still loading, so it denied even an entitled admin (deny-by-default) and bounced to /login. It's now async: awaits AccessStore.whenReady() (new — resolves once /me settles), then allows if entitled; an authenticated-but-unentitled user goes to /dashboard, anonymous to /login. + auth.guard.spec (the missing test that let this ship). - **Dev role wasn't sticky.** currentRole() read ?role= from the URL on every request, but login/nav drop the param, silently reverting admin→drafter mid-session and 403-ing the admin endpoints. It now persists the role per-tab (sessionStorage), so every role-aware request keeps it. Dev-only (the interceptor is wired only under isDevMode). - **No way in.** Added capability-gated Huisstijl + Stamdata links to the header (shown only when /me grants the cap); injecting AccessStore there also warms /me early. New en translations for the two labels; site-header story stubs AccessStore (+ AsAdmin variant) so it needs no HTTP. Verified live: with ?role=admin the header shows both links, clicking Stamdata loads the grid (GET /api/v1/stamdata → 200, was 403→redirect); a non-admin sees no link. Full `npm run ci` green (310 tests); site-header stories pass axe. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { Injectable, computed, inject } from '@angular/core';
|
||||
import { toObservable } from '@angular/core/rxjs-interop';
|
||||
import { filter, firstValueFrom } from 'rxjs';
|
||||
import { RemoteData, fromResource } from '@shared/application/remote-data';
|
||||
import { Capability } from '@shared/domain/capability';
|
||||
import { MeAdapter, parseMe } from '@shared/infrastructure/me.adapter';
|
||||
@@ -40,4 +42,13 @@ export class AccessStore {
|
||||
const tag = this.capabilities().tag;
|
||||
return tag === 'Success' || tag === 'Failure';
|
||||
});
|
||||
|
||||
private ready$ = toObservable(this.ready);
|
||||
/** Resolves once `/me` has settled (success or failure). The `capabilityGuard` awaits
|
||||
this before deciding — otherwise it reads `can()` while `/me` is still loading and
|
||||
wrongly denies (deny-by-default), bouncing even an entitled user. */
|
||||
async whenReady(): Promise<void> {
|
||||
if (this.ready()) return;
|
||||
await firstValueFrom(this.ready$.pipe(filter((r) => r)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user