Restructures into apps/ssp + apps/behandelportal (two Angular projects) plus libs/shared + libs/beheer (cross-app libraries), replacing WP-61's separate sibling repo. That split had already produced real drift: a hand-vendored copy of the backend's OpenAPI doc, a shared/ui+layout tree forked and silently diverging (7 files), and beheer + the styles.scss token bridge duplicated byte-for-byte across both repos. - git mv the SSP's src/app/* into apps/ssp/; fold shared/, beheer/, environments/, the Storybook docs/*.mdx, and styles.scss into libs/shared + libs/beheer (all confirmed identical between the two repos before merging). auth stays deliberately duplicated per ADR-0002 (actor-specific, expected to diverge) - amended there. - One generated API client (libs/shared), no more vendored swagger.json. - .dependency-cruiser split into a base factory + one config per app, and Storybook into .storybook-ssp/.storybook-behandelportal - both forced by the @auth/* alias resolving to different directories per app. - SiteHeaderComponent/ShellComponent gained HEADER_NAV_ITEMS/ HEADER_ADMIN_LINKS/DEBUG_PANEL injection tokens so each app supplies its own nav/admin-links/dev-panel instead of one being hardcoded. - CLAUDE.md, ARCHITECTURE.md, dependencies.md, and ADR-0002 updated; WP-67 backlog entry documents the full decision trail. npm run ci green (lint, dep:check x2, 360 tests across ssp/ behandelportal/shared/beheer, both localized builds, backend tests, snippet + api-client drift); both dev servers, both Storybook instances, and docker compose verified working. The old sibling repo (/home/eho/repos/behandelportal) is left untouched, not deleted. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
62 lines
2.4 KiB
TypeScript
62 lines
2.4 KiB
TypeScript
import { Injectable, computed, effect, inject, signal } from '@angular/core';
|
|
import { Result } from '@shared/kernel/fp';
|
|
import { Session } from '../domain/session';
|
|
import { DigidAdapter } from '../infrastructure/digid.adapter';
|
|
|
|
const STORAGE_KEY = 'session-v1';
|
|
|
|
/** Restore a persisted session (best-effort; corrupt entry → logged out).
|
|
G2: validate the shape before trusting it. G1: the BSN is never persisted
|
|
(see the effect below), so a restored session carries an empty one — it is
|
|
unused after login; only `naam` is shown in the chrome. */
|
|
function restore(): Session | null {
|
|
try {
|
|
const raw = localStorage.getItem(STORAGE_KEY);
|
|
if (!raw) return null;
|
|
const parsed = JSON.parse(raw) as Partial<Session>;
|
|
return typeof parsed?.naam === 'string' ? { bsn: '', naam: parsed.naam } : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Holds the current session for the whole app. Because it is providedIn:'root'
|
|
* there is exactly one instance — every component that injects it sees the same
|
|
* session signal, so logging in is instantly visible everywhere (the guard, the
|
|
* header, etc.). The session is mirrored to localStorage so a refresh, a deep-link,
|
|
* or the full-page navigation the language switch performs (nl at `/` ⇄ en at `/en/`,
|
|
* separate bundles) keeps you logged in. ponytail: localStorage, not sessionStorage —
|
|
* sessionStorage's per-tab clearing dropped the login on the cross-bundle language
|
|
* switch. Trade-off: the demo session now survives tab close; a real portal keeps auth
|
|
* in an httpOnly cookie/token, not web storage.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class SessionStore {
|
|
private digid = inject(DigidAdapter);
|
|
private _session = signal<Session | null>(restore());
|
|
|
|
readonly session = this._session.asReadonly();
|
|
readonly isAuthenticated = computed(() => this._session() !== null);
|
|
|
|
constructor() {
|
|
effect(() => {
|
|
const s = this._session();
|
|
// G1: persist only `naam` — never write the BSN (national ID) to storage.
|
|
if (s) localStorage.setItem(STORAGE_KEY, JSON.stringify({ naam: s.naam }));
|
|
else localStorage.removeItem(STORAGE_KEY);
|
|
});
|
|
}
|
|
|
|
/** Effectful command: authenticate, then store the session on success. */
|
|
async login(bsn: string): Promise<Result<string, Session>> {
|
|
const r = await this.digid.authenticate(bsn);
|
|
if (r.ok) this._session.set(r.value);
|
|
return r;
|
|
}
|
|
|
|
logout() {
|
|
this._session.set(null);
|
|
}
|
|
}
|