test(portal-self-service): DigiD-guarded registration submit page (refs #67)
Scaffold libs/ui (NL Design System via Utrecht components) and libs/auth (DigiD OIDC over angular-auth-oidc-client: mockable AuthService, provider, token interceptor, authenticated guard). Failing component + axe tests for the RegistrationPage: it must show the signed-in BSN, submit to the BFF (mocked api-client) and confirm, with no WCAG 2.1 AA violations. The page is a stub, so the behaviour tests fail; green follows. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
libs/auth/README.md
Normal file
7
libs/auth/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# auth
|
||||
|
||||
This library was generated with [Nx](https://nx.dev).
|
||||
|
||||
## Running unit tests
|
||||
|
||||
Run `nx test auth` to execute the unit tests.
|
||||
34
libs/auth/eslint.config.mjs
Normal file
34
libs/auth/eslint.config.mjs
Normal file
@@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'lib',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'lib',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
13
libs/auth/project.json
Normal file
13
libs/auth/project.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "auth",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/auth/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [],
|
||||
"targets": {
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
4
libs/auth/src/index.ts
Normal file
4
libs/auth/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './lib/auth.service';
|
||||
export * from './lib/digid-auth.service';
|
||||
export * from './lib/digid-auth.providers';
|
||||
export * from './lib/authenticated.guard';
|
||||
16
libs/auth/src/lib/auth.service.ts
Normal file
16
libs/auth/src/lib/auth.service.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Signal } from '@angular/core';
|
||||
|
||||
/**
|
||||
* The portal's view of the signed-in user. An abstraction over the OIDC library so components and
|
||||
* guards depend on a small, mockable surface (the real implementation is DigiadAuthService).
|
||||
*/
|
||||
export abstract class AuthService {
|
||||
/** Whether a DigiD session is active. */
|
||||
abstract readonly isAuthenticated: Signal<boolean>;
|
||||
/** The citizen-service number from the DigiD token, once authenticated. */
|
||||
abstract readonly bsn: Signal<string | undefined>;
|
||||
/** Start the DigiD login (redirects to Keycloak). */
|
||||
abstract login(): void;
|
||||
/** End the session. */
|
||||
abstract logout(): void;
|
||||
}
|
||||
13
libs/auth/src/lib/authenticated.guard.ts
Normal file
13
libs/auth/src/lib/authenticated.guard.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { inject } from '@angular/core';
|
||||
import { CanActivateFn } from '@angular/router';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
/** Allow the route only when a DigiD session is active; otherwise start the login. */
|
||||
export const authenticatedGuard: CanActivateFn = () => {
|
||||
const auth = inject(AuthService);
|
||||
if (auth.isAuthenticated()) {
|
||||
return true;
|
||||
}
|
||||
auth.login();
|
||||
return false;
|
||||
};
|
||||
40
libs/auth/src/lib/digid-auth.providers.ts
Normal file
40
libs/auth/src/lib/digid-auth.providers.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { EnvironmentProviders, makeEnvironmentProviders } from '@angular/core';
|
||||
import { authInterceptor, LogLevel, provideAuth } from 'angular-auth-oidc-client';
|
||||
import { AuthService } from './auth.service';
|
||||
import { DigiadAuthService } from './digid-auth.service';
|
||||
|
||||
export interface DigiadAuthOptions {
|
||||
/** The Keycloak `digid` realm issuer, as reachable from the browser. */
|
||||
authority: string;
|
||||
/** Where Keycloak redirects back to after login (usually the app origin). */
|
||||
redirectUrl: string;
|
||||
/** The BFF origin whose requests get the bearer token attached (secure route). */
|
||||
secureApiOrigin: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure DigiD login (Keycloak `digid` realm, public client `big-portal`, auth-code + PKCE) and
|
||||
* bind {@link AuthService} to the OIDC-backed implementation. Register {@link authInterceptor} in the
|
||||
* app's HttpClient so BFF calls carry the token.
|
||||
*/
|
||||
export function provideDigiadAuth(options: DigiadAuthOptions): EnvironmentProviders {
|
||||
return makeEnvironmentProviders([
|
||||
provideAuth({
|
||||
config: {
|
||||
authority: options.authority,
|
||||
redirectUrl: options.redirectUrl,
|
||||
postLogoutRedirectUri: options.redirectUrl,
|
||||
clientId: 'big-portal',
|
||||
scope: 'openid profile',
|
||||
responseType: 'code',
|
||||
silentRenew: true,
|
||||
useRefreshToken: true,
|
||||
secureRoutes: [options.secureApiOrigin],
|
||||
logLevel: LogLevel.Warn,
|
||||
},
|
||||
}),
|
||||
{ provide: AuthService, useClass: DigiadAuthService },
|
||||
]);
|
||||
}
|
||||
|
||||
export { authInterceptor };
|
||||
29
libs/auth/src/lib/digid-auth.service.ts
Normal file
29
libs/auth/src/lib/digid-auth.service.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { inject, Injectable, Signal } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { OidcSecurityService } from 'angular-auth-oidc-client';
|
||||
import { map } from 'rxjs';
|
||||
import { AuthService } from './auth.service';
|
||||
|
||||
/** DigiD-backed AuthService over angular-auth-oidc-client (Keycloak `digid` realm). */
|
||||
@Injectable()
|
||||
export class DigiadAuthService extends AuthService {
|
||||
private readonly oidc = inject(OidcSecurityService);
|
||||
|
||||
readonly isAuthenticated: Signal<boolean> = toSignal(
|
||||
this.oidc.isAuthenticated$.pipe(map((result) => result.isAuthenticated)),
|
||||
{ initialValue: false },
|
||||
);
|
||||
|
||||
readonly bsn: Signal<string | undefined> = toSignal(
|
||||
this.oidc.userData$.pipe(map((data) => (data.userData as { bsn?: string } | null)?.bsn)),
|
||||
{ initialValue: undefined },
|
||||
);
|
||||
|
||||
override login(): void {
|
||||
this.oidc.authorize();
|
||||
}
|
||||
|
||||
override logout(): void {
|
||||
this.oidc.logoff().subscribe();
|
||||
}
|
||||
}
|
||||
5
libs/auth/src/test-setup.ts
Normal file
5
libs/auth/src/test-setup.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-snapshots';
|
||||
import { setupTestBed } from '@analogjs/vitest-angular/setup-testbed';
|
||||
|
||||
setupTestBed({ zoneless: false });
|
||||
31
libs/auth/tsconfig.json
Normal file
31
libs/auth/tsconfig.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
26
libs/auth/tsconfig.lib.json
Normal file
26
libs/auth/tsconfig.lib.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/test-setup.ts"
|
||||
]
|
||||
}
|
||||
29
libs/auth/tsconfig.spec.json
Normal file
29
libs/auth/tsconfig.spec.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
28
libs/auth/vite.config.mts
Normal file
28
libs/auth/vite.config.mts
Normal file
@@ -0,0 +1,28 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/libs/auth',
|
||||
plugins: [angular(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: () => [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
name: 'auth',
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../coverage/libs/auth',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user