feat(portal-admin): spa auth wiring + admin shell skeleton
Phase-3a step per ADR-0020 §"Confirmation" item 4 (entry route + admin shell). Wires the existing `feature-auth` library against the distinct admin OIDC routes (`/api/admin/auth/*`) the BFF exposes since PR #129, ships a lean header/sidebar/footer chrome with an "Admin" badge so an internal user can never mistake the surface for portal-shell, and gives the landing page a self-test panel that confirms the auth chain end-to-end as soon as an `admin` Entra role gets assigned. Lib changes (libs/feature/auth) - New AUTH_PATH_PREFIX injection token. Default factory returns `/auth` so portal-shell-shaped hosts keep working without an explicit provider. Admin hosts override with `/admin/auth`. - AuthService composes URLs as `${bffBaseUrl}${pathPrefix}/{me,login, logout}`. The interceptors are unchanged — they only care about the BFF base URL. portal-admin wiring - environment.ts: same shape as portal-shell, same BFF base URL (both SPAs talk to one BFF per ADR-0020 §"Where does the admin app live"), same CSRF cookie name in v1. - app.config.ts: HttpClient with the standard interceptor chain (credentials → csrf → unauthorized), AUTH_BFF_BASE_URL from env, AUTH_PATH_PREFIX = '/admin/auth', AUTH_CSRF_COOKIE_NAME from env. Admin shell components - AdminHeader: APF wordmark + persistent "Admin" badge + auth widget. No global search / notifications / help cluster — admins land on tabular workloads, not discovery. Per ADR-0020 §"UX style is data-dense". - AdminSidebar: static menu listing the four ADR-0020 v1 modules. Audit log is a live router link (target of PR 8); the others are aria-disabled placeholders with a "Soon" badge so the navigation shape is visible even before they ship. - AdminFooter: copyright + persistent "Admin surface" tag — the badge stays in view for long workloads where the header may have scrolled off. Home page - Auth self-test panel: signed-in payload (display name, tenant, oid) when authenticated, Sign in button when anonymous, error state when the BFF is unreachable. - Roadmap list quoting ADR-0020's v1 catalogue so a returning contributor sees what's pending without grepping. Notes - LayoutStateService isn't consumed yet (no collapse toggle in v1), but the theme preference still threads through because both apps read the same localStorage key — toggle in portal-shell, see it honoured in portal-admin. - Strings are plain English in v1. The admin app's $localize plumbing is wired but adding markers to the shell chrome here would require regenerating messages.fr.xlf and i18nMissingTranslation=error fails the prod build on every gap. Full i18n is its own follow-up. - v1 reuses the shared `portal_csrf` cookie name. A user with both portals open could see CSRF cookie overwrites on session creation; splitting to `portal_admin_csrf` is a follow-up if the pattern becomes common. - Build verified locally: 86.65 KB gzip initial / 300 KB budget, 4.46 KB CSS / 150 KB budget. Tests: +15 specs (AdminHeader 5, AdminSidebar 3, AdminFooter 2, Home 4, App 1 expanded). 1 new test in feature-auth AuthService for the AUTH_PATH_PREFIX override path.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { AUTH_BFF_BASE_URL, AUTH_NAVIGATOR } from './auth.config';
|
||||
import { AUTH_BFF_BASE_URL, AUTH_NAVIGATOR, AUTH_PATH_PREFIX } from './auth.config';
|
||||
import { AuthService } from './auth.service';
|
||||
import type { CurrentUser } from './auth.types';
|
||||
|
||||
@@ -98,13 +98,36 @@ describe('AuthService', () => {
|
||||
});
|
||||
|
||||
describe('URL accessors', () => {
|
||||
it('derives me / login / logout URLs from the injected base', () => {
|
||||
it('derives me / login / logout URLs from the injected base + default /auth prefix', () => {
|
||||
const { service, http } = setup();
|
||||
expect(service.meUrl).toBe(`${BFF_BASE}/auth/me`);
|
||||
expect(service.loginUrl).toBe(`${BFF_BASE}/auth/login`);
|
||||
expect(service.logoutUrl).toBe(`${BFF_BASE}/auth/logout`);
|
||||
http.expectOne(ME_URL).flush({}, { status: 401, statusText: 'Unauthorized' });
|
||||
});
|
||||
|
||||
it('honours AUTH_PATH_PREFIX when the host provides a non-default value (admin surface)', () => {
|
||||
const navigate = vi.fn();
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{ provide: AUTH_BFF_BASE_URL, useValue: BFF_BASE },
|
||||
{ provide: AUTH_NAVIGATOR, useValue: navigate },
|
||||
{ provide: AUTH_PATH_PREFIX, useValue: '/admin/auth' },
|
||||
AuthService,
|
||||
],
|
||||
});
|
||||
const service = TestBed.inject(AuthService);
|
||||
const http = TestBed.inject(HttpTestingController);
|
||||
expect(service.meUrl).toBe(`${BFF_BASE}/admin/auth/me`);
|
||||
expect(service.loginUrl).toBe(`${BFF_BASE}/admin/auth/login`);
|
||||
expect(service.logoutUrl).toBe(`${BFF_BASE}/admin/auth/logout`);
|
||||
// The auto-refresh on first inject fires against the override path.
|
||||
http
|
||||
.expectOne(`${BFF_BASE}/admin/auth/me`)
|
||||
.flush({}, { status: 401, statusText: 'Unauthorized' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('login() / logout()', () => {
|
||||
|
||||
Reference in New Issue
Block a user