@abgov/nx-adsp 12.4.1 → 12.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/generators.json +6 -0
  2. package/package.json +1 -1
  3. package/src/generators/angular-app/angular-app.js +22 -7
  4. package/src/generators/angular-app/angular-app.js.map +1 -1
  5. package/src/generators/angular-app/files/AGENTS.md__tmpl__ +96 -0
  6. package/src/generators/angular-app/files/src/app/app.component.css__tmpl__ +10 -54
  7. package/src/generators/angular-app/files/src/app/app.component.html__tmpl__ +18 -49
  8. package/src/generators/angular-app/files/src/app/app.component.spec.ts__tmpl__ +22 -27
  9. package/src/generators/angular-app/files/src/app/app.component.ts__tmpl__ +60 -20
  10. package/src/generators/angular-app/files/src/app/app.config.ts__tmpl__ +27 -0
  11. package/src/generators/angular-app/files/src/app/app.routes.ts__tmpl__ +10 -31
  12. package/src/generators/angular-app/files/src/app/home/home.component.html__tmpl__ +1 -3
  13. package/src/generators/angular-app/files/src/app/home/home.component.ts__tmpl__ +9 -23
  14. package/src/generators/angular-app/files/src/app/protected/protected.component.html__tmpl__ +2 -2
  15. package/src/generators/angular-app/files/src/app/protected/protected.component.spec.ts__tmpl__ +27 -43
  16. package/src/generators/angular-app/files/src/app/protected/protected.component.ts__tmpl__ +15 -15
  17. package/src/generators/angular-app/files/src/app/services/auth-guard.service.ts__tmpl__ +12 -21
  18. package/src/generators/angular-app/files/src/environments/environment.ts__tmpl__ +2 -11
  19. package/src/generators/angular-app/files/src/index.html__tmpl__ +3 -9
  20. package/src/generators/angular-app/files/src/main.ts__tmpl__ +8 -12
  21. package/src/generators/angular-app/files/src/silent-check-sso.html__tmpl__ +5 -0
  22. package/src/generators/angular-app/files/src/styles.css__tmpl__ +2 -2
  23. package/src/generators/express-service/files/AGENTS.md__tmpl__ +76 -0
  24. package/src/generators/mean/mean.d.ts +3 -0
  25. package/src/generators/mean/mean.js +30 -0
  26. package/src/generators/mean/mean.js.map +1 -0
  27. package/src/generators/mean/mean.spec.ts +38 -0
  28. package/src/generators/mean/schema.d.ts +11 -0
  29. package/src/generators/mean/schema.json +26 -0
  30. package/src/generators/react-app/files/AGENTS.md__tmpl__ +76 -0
  31. package/src/generators/angular-app/files/src/app/app.module.ts__tmpl__ +0 -47
  32. package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.css__tmpl__ +0 -13
  33. package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.html__tmpl__ +0 -12
  34. package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.spec.ts__tmpl__ +0 -33
  35. package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.ts__tmpl__ +0 -48
  36. package/src/generators/angular-app/files/src/app/auth.interceptor.ts__tmpl__ +0 -24
  37. package/src/generators/angular-app/files/src/app/logout/logout.component.html__tmpl__ +0 -1
  38. package/src/generators/angular-app/files/src/app/logout/logout.component.ts__tmpl__ +0 -9
  39. package/src/generators/angular-app/files/src/app/services/auth.service.ts__tmpl__ +0 -57
  40. package/src/generators/angular-app/files/src/app/tenant.service.ts__tmpl__ +0 -19
  41. package/src/generators/angular-app/files/src/environments/config.ts__tmpl__ +0 -21
  42. /package/src/generators/angular-app/files/{src → public}/assets/banner.jpg +0 -0
  43. /package/src/generators/angular-app/files/{src → public}/assets/github-1.svg +0 -0
  44. /package/src/generators/angular-app/files/{src → public}/favicon.ico +0 -0
@@ -1,52 +1,36 @@
1
- import { ComponentFixture, TestBed, waitForAsync, inject } from '@angular/core/testing';
1
+ import { TestBed } from '@angular/core/testing';
2
+ import { provideHttpClient } from '@angular/common/http';
3
+ import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
4
+ import { Keycloak } from 'keycloak-angular';
2
5
  import { ProtectedComponent } from './protected.component';
3
- import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
4
- import TenantService from '../tenant.service';
5
- import {
6
- BrowserDynamicTestingModule,
7
- platformBrowserDynamicTesting
8
- } from "@angular/platform-browser-dynamic/testing";
9
6
 
10
7
  describe('ProtectedComponent', () => {
11
- let fixture: ComponentFixture<ProtectedComponent>;
12
- let httpTestingController: HttpTestingController;
13
- let envData = {"production":false,"access":{"url":"https://testurl.com","realm":"123","client_id":"urn:ads:platform:tenant-admin-app"},"tenantApi":{"host":"http://localhost:3333","endpoints":{"tenantNameByRealm":"/api/tenant/v1/realm"}}}
14
-
15
- localStorage.setItem('envData', JSON.stringify(envData));
16
- beforeAll( ()=> {
17
- TestBed.initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());
8
+ const keycloakMock = {
9
+ authenticated: true,
10
+ tokenParsed: { name: 'Test User' },
11
+ };
12
+
13
+ let httpTesting: HttpTestingController;
14
+
15
+ beforeEach(async () => {
16
+ await TestBed.configureTestingModule({
17
+ imports: [ProtectedComponent],
18
+ providers: [
19
+ provideHttpClient(),
20
+ provideHttpClientTesting(),
21
+ { provide: Keycloak, useValue: keycloakMock },
22
+ ],
23
+ }).compileComponents();
24
+
25
+ httpTesting = TestBed.inject(HttpTestingController);
18
26
  });
19
- beforeEach(
20
- waitForAsync(() => {
21
- TestBed.configureTestingModule({
22
- declarations: [ProtectedComponent],
23
- providers: [ProtectedComponent, TenantService],
24
- imports: [HttpClientTestingModule],
25
- }).compileComponents();
26
27
 
27
- httpTestingController = TestBed.inject(HttpTestingController);
28
- })
29
- );
28
+ afterEach(() => httpTesting.verify());
30
29
 
31
- beforeEach(() => {
32
- fixture = TestBed.createComponent(ProtectedComponent);
30
+ it('should create', () => {
31
+ const fixture = TestBed.createComponent(ProtectedComponent);
33
32
  fixture.detectChanges();
33
+ httpTesting.expectOne('/api/v1/private').flush({ message: 'Hello' });
34
+ expect(fixture.componentInstance).toBeTruthy();
34
35
  });
35
-
36
- afterEach(() => {
37
- httpTestingController.verify();
38
- });
39
-
40
- it('should create', (inject([TenantService, HttpTestingController],
41
- () => {
42
- const mockTenant = { status: 200, statusText: 'OK'}
43
- const data = { name: 'Child Services' }
44
- const req = httpTestingController.expectOne(
45
- 'http://localhost:3333/api/tenant/v1/realm/123'
46
- )
47
-
48
- expect(req.request.method).toEqual('GET');
49
-
50
- req.flush(data, mockTenant);
51
- })));
52
36
  });
@@ -1,27 +1,27 @@
1
- import { Component, OnInit } from '@angular/core';
2
- import TenantService from '../tenant.service';
1
+ import { Component, inject, OnInit, signal } from '@angular/core';
2
+ import { HttpClient } from '@angular/common/http';
3
+ import Keycloak from 'keycloak-js';
3
4
 
4
5
  @Component({
5
- selector: 'app-protected',
6
+ selector: '<%= projectName %>-protected',
7
+ standalone: true,
6
8
  templateUrl: './protected.component.html',
7
- styleUrls: ['./protected.component.css'],
9
+ styleUrl: './protected.component.css',
8
10
  })
9
11
  export class ProtectedComponent implements OnInit {
10
- constructor(private _tenantService: TenantService) {}
12
+ private http = inject(HttpClient);
13
+ private keycloak = inject(Keycloak);
11
14
 
12
- public tenant = { name: '' };
15
+ privateMessage = signal('Not retrieved');
13
16
 
14
- getTenant() {
15
- this._tenantService.getTenant().subscribe(
16
- (data: any) => {
17
- this.tenant = data.tenant;
18
- },
19
- (err) => console.error(err),
20
- () => console.log('done loading tenant')
21
- );
17
+ get userName(): string {
18
+ return (this.keycloak.tokenParsed?.['name'] as string) ?? '';
22
19
  }
23
20
 
24
21
  ngOnInit() {
25
- this.getTenant();
22
+ this.http.get<{ message: string }>('/api/v1/private').subscribe({
23
+ next: (data) => this.privateMessage.set(data.message),
24
+ error: () => this.privateMessage.set('Error loading data'),
25
+ });
26
26
  }
27
27
  }
@@ -1,21 +1,12 @@
1
- import { Injectable } from '@angular/core';
2
- import { CanActivate } from '@angular/router';
3
- import { AuthService } from './auth.service';
4
-
5
- @Injectable({
6
- providedIn: 'root'
7
- })
8
- export class AuthGuardService implements CanActivate {
9
-
10
- constructor(private authService: AuthService) { }
11
-
12
- canActivate(): boolean {
13
- if (this.authService.isLoggedIn()) {
14
- return true;
15
- }
16
-
17
- console.log('start authentication...');
18
- this.authService.startAuthentication();
19
- return false;
20
- }
21
- }
1
+ // Auth guard is configured in app.routes.ts using keycloak-angular's createAuthGuard.
2
+ // Add this file if you need a reusable, injectable guard:
3
+ //
4
+ // import { Injectable } from '@angular/core';
5
+ // import { Router } from '@angular/router';
6
+ // import { createAuthGuard } from 'keycloak-angular';
7
+ //
8
+ // export const authGuard = createAuthGuard(async ({ keycloak }) => {
9
+ // if (keycloak.authenticated) return true;
10
+ // await keycloak.login({ redirectUri: window.location.href });
11
+ // return false;
12
+ // });
@@ -1,17 +1,8 @@
1
- // This file can be replaced during build by using the `fileReplacements` array.
2
- // When building for production, this file is replaced with `environment.prod.ts`.
3
-
4
1
  export const environment = {
5
2
  production: false,
6
3
  access: {
7
- url: '<%= accessServiceUrl %>',
4
+ url: '<%= accessServiceUrl %>/auth',
8
5
  realm: '<%= tenantRealm %>',
9
- client_id: 'urn:ads:<%= tenant %>:<%= projectName %>'
6
+ client_id: 'urn:ads:<%= tenant %>:<%= projectName %>',
10
7
  },
11
- tenantApi: {
12
- host: "http://localhost:3333",
13
- endpoints: {
14
- tenantNameByRealm: "/api/tenant/v1/realm",
15
- }
16
- }
17
8
  };
@@ -6,16 +6,10 @@
6
6
  <base href="/" />
7
7
  <meta name="viewport" content="width=device-width, initial-scale=1" />
8
8
  <link rel="icon" type="image/x-icon" href="favicon.ico" />
9
- <script
10
- type="module"
11
- src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"
12
- ></script>
13
- <script
14
- nomodule
15
- src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"
16
- ></script>
9
+ <script type="module" src="https://cdn.jsdelivr.net/npm/ionicons@latest/dist/ionicons/ionicons.esm.js"></script>
10
+ <script nomodule src="https://cdn.jsdelivr.net/npm/ionicons@latest/dist/ionicons/ionicons.js"></script>
17
11
  </head>
18
12
  <body>
19
- <<%= projectName %>-app-root></<%= projectName %>-app-root>
13
+ <<%= projectName %>-root></<%= projectName %>-root>
20
14
  </body>
21
15
  </html>
@@ -1,13 +1,9 @@
1
- import { enableProdMode } from '@angular/core';
2
- import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
1
+ import 'zone.js';
2
+ import '@abgov/web-components';
3
+ import { bootstrapApplication } from '@angular/platform-browser';
4
+ import { AppComponent } from './app/app.component';
5
+ import { appConfig } from './app/app.config';
3
6
 
4
- import { AppModule } from './app/app.module';
5
- import { environment } from './environments/environment';
6
-
7
- if (environment.production) {
8
- enableProdMode();
9
- }
10
-
11
- platformBrowserDynamic()
12
- .bootstrapModule(AppModule)
13
- .catch((err) => console.error(err));
7
+ bootstrapApplication(AppComponent, appConfig).catch((err) =>
8
+ console.error(err)
9
+ );
@@ -0,0 +1,5 @@
1
+ <html>
2
+ <body>
3
+ <script>parent.postMessage(location.href, location.origin);</script>
4
+ </body>
5
+ </html>
@@ -1,6 +1,6 @@
1
- /* You can add global styles to this file, and also import other style files */
1
+ @import "@abgov/design-tokens/dist/tokens.css";
2
2
  @import "@abgov/web-components/index.css";
3
3
 
4
4
  body {
5
- margin: 0px;
5
+ margin: 0;
6
6
  }
@@ -0,0 +1,76 @@
1
+ # AGENTS.md — <%= projectName %>
2
+
3
+ Node/Express backend service for the Alberta Digital Service Platform (ADSP).
4
+ Generated by `nx g @abgov/nx-adsp:express-service`.
5
+
6
+ ## Stack
7
+
8
+ - **Runtime**: Node.js + Express
9
+ - **Auth**: passport.js with tenant strategy from `@abgov/adsp-service-sdk`
10
+ - **Config**: `envalid` with `.env` file (see `src/environment.ts`)
11
+ - **SDK**: `@abgov/adsp-service-sdk` — provides service registration, auth,
12
+ event publishing, configuration management, and service discovery
13
+
14
+ ## Key files
15
+
16
+ | File | Purpose |
17
+ |------|---------|
18
+ | `src/main.ts` | App entry — SDK init, middleware, routes, server start |
19
+ | `src/environment.ts` | Validated env config with defaults pre-set from ADSP tenant |
20
+
21
+ ## SDK capabilities
22
+
23
+ `initializeService()` returns `capabilities`:
24
+
25
+ ```typescript
26
+ const { logger, tenantStrategy, traceHandler, configurationHandler,
27
+ healthCheck, directory, tokenProvider, eventService } = capabilities;
28
+ ```
29
+
30
+ To resolve another ADSP service URL:
31
+
32
+ ```typescript
33
+ const url = await directory.getServiceUrl(AdspId.parse('urn:ads:platform:file-service'));
34
+ ```
35
+
36
+ To call another service with an access token:
37
+
38
+ ```typescript
39
+ const token = await tokenProvider.getAccessToken();
40
+ ```
41
+
42
+ ## Adding routes
43
+
44
+ Add route handlers in `main.ts` under the `/<%= projectName %>/v1` path prefix:
45
+
46
+ ```typescript
47
+ app.get('/<%= projectName %>/v1/my-resource', (req, res) => {
48
+ const user = req.user; // null for anonymous, populated for authenticated
49
+ res.json({ ... });
50
+ });
51
+ ```
52
+
53
+ `passport.authenticate(['tenant', 'anonymous'])` is applied to the entire
54
+ `/<%= projectName %>/v1` prefix — check `req.user` inside handlers to
55
+ distinguish authenticated from anonymous access.
56
+
57
+ ## Frontend proxy integration (mern / mean stacks)
58
+
59
+ When this service is paired with a React or Angular frontend via the `mern` or
60
+ `mean` generators, the frontend proxies `/api/` to this service:
61
+
62
+ - **Dev** (webpack / Vite): `/api/v1/my-resource` → `/<%= projectName %>/v1/my-resource` on `localhost:3333`
63
+ - **Production** (nginx): same rewrite via the nginx proxy block in the frontend app
64
+
65
+ All API routes in this service live under `/<%= projectName %>/v1/`. The proxy
66
+ rewrites the frontend's `/api/` prefix to `/<%= projectName %>/` before the
67
+ request reaches Express, so `/<%= projectName %>/v1/public` maps to the
68
+ frontend's `/api/v1/public`.
69
+
70
+ ## What NOT to change
71
+
72
+ - `initializeService(...)` config — `CLIENT_ID` and `CLIENT_SECRET` are read
73
+ from environment; do not hardcode credentials
74
+ - The passport strategy setup — the tenant strategy handles JWT validation
75
+ - `configurationHandler` on the API path — provides configuration service
76
+ integration; keep it applied before route handlers
@@ -0,0 +1,3 @@
1
+ import { Tree } from '@nx/devkit';
2
+ import { Schema } from './schema';
3
+ export default function (host: Tree, options: Schema): Promise<() => void>;
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = default_1;
4
+ const tslib_1 = require("tslib");
5
+ const devkit_1 = require("@nx/devkit");
6
+ const nx_oc_1 = require("@abgov/nx-oc");
7
+ const angular_app_1 = require("../angular-app/angular-app");
8
+ const express_service_1 = require("../express-service/express-service");
9
+ function normalizeOptions(host, options) {
10
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
11
+ const adsp = yield (0, nx_oc_1.getAdspConfiguration)(host, options);
12
+ return Object.assign(Object.assign({}, options), { adsp });
13
+ });
14
+ }
15
+ function default_1(host, options) {
16
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
17
+ const normalizedOptions = yield normalizeOptions(host, options);
18
+ const projectName = (0, devkit_1.names)(options.name).fileName;
19
+ yield (0, express_service_1.default)(host, Object.assign(Object.assign({}, normalizedOptions), { name: `${projectName}-service` }));
20
+ yield (0, angular_app_1.default)(host, Object.assign(Object.assign({}, normalizedOptions), { name: `${projectName}-app`, proxy: {
21
+ location: '/api/',
22
+ proxyPass: `http://${projectName}-service:3333/${projectName}-service/`,
23
+ } }));
24
+ yield (0, devkit_1.formatFiles)(host);
25
+ return () => {
26
+ (0, devkit_1.installPackagesTask)(host);
27
+ };
28
+ });
29
+ }
30
+ //# sourceMappingURL=mean.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mean.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/mean/mean.ts"],"names":[],"mappings":";;AAcA,4BAuBC;;AArCD,uCAA2E;AAC3E,wCAAoD;AACpD,4DAAwD;AACxD,wEAAoE;AAGpE,SAAe,gBAAgB,CAC7B,IAAU,EACV,OAAe;;QAEf,MAAM,IAAI,GAAG,MAAM,IAAA,4BAAoB,EAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QACvD,uCAAY,OAAO,KAAE,IAAI,IAAG;IAC9B,CAAC;CAAA;AAED,mBAA+B,IAAU,EAAE,OAAe;;QACxD,MAAM,iBAAiB,GAAG,MAAM,gBAAgB,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAChE,MAAM,WAAW,GAAG,IAAA,cAAK,EAAC,OAAO,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC;QAEjD,MAAM,IAAA,yBAAkB,EAAC,IAAI,kCACxB,iBAAiB,KACpB,IAAI,EAAE,GAAG,WAAW,UAAU,IAC9B,CAAC;QAEH,MAAM,IAAA,qBAAc,EAAC,IAAI,kCACpB,iBAAiB,KACpB,IAAI,EAAE,GAAG,WAAW,MAAM,EAC1B,KAAK,EAAE;gBACL,QAAQ,EAAE,OAAO;gBACjB,SAAS,EAAE,UAAU,WAAW,iBAAiB,WAAW,WAAW;aACxE,IACD,CAAC;QAEH,MAAM,IAAA,oBAAW,EAAC,IAAI,CAAC,CAAC;QAExB,OAAO,GAAG,EAAE;YACV,IAAA,4BAAmB,EAAC,IAAI,CAAC,CAAC;QAC5B,CAAC,CAAC;IACJ,CAAC;CAAA"}
@@ -0,0 +1,38 @@
1
+ import { readProjectConfiguration } from '@nx/devkit';
2
+ import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
3
+ import * as utils from '@abgov/nx-oc';
4
+ import { environments } from '@abgov/nx-oc';
5
+ import { Schema } from './schema';
6
+ import generator from './mean';
7
+
8
+ jest.mock('@abgov/nx-oc');
9
+ const utilsMock = utils as jest.Mocked<typeof utils>;
10
+ utilsMock.getAdspConfiguration.mockResolvedValue({
11
+ tenant: 'test',
12
+ tenantRealm: 'test',
13
+ accessServiceUrl: environments.test.accessServiceUrl,
14
+ directoryServiceUrl: environments.test.directoryServiceUrl,
15
+ });
16
+ utilsMock.deploymentGenerator.mockResolvedValue(undefined);
17
+
18
+ describe('MEAN Generator', () => {
19
+ const options: Schema = {
20
+ name: 'test',
21
+ env: 'dev',
22
+ };
23
+
24
+ it('can run', async () => {
25
+ const host = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
26
+ await generator(host, options);
27
+
28
+ const appConfig = readProjectConfiguration(host, 'test-app');
29
+ expect(appConfig.root).toBe('apps/test-app');
30
+
31
+ const serviceConfig = readProjectConfiguration(host, 'test-service');
32
+ expect(serviceConfig.root).toBe('apps/test-service');
33
+
34
+ expect(host.exists('apps/test-app/nginx.conf')).toBeTruthy();
35
+ const nginxConf = host.read('apps/test-app/nginx.conf').toString();
36
+ expect(nginxConf).toContain('http://test-service:3333/');
37
+ }, 30000);
38
+ });
@@ -0,0 +1,11 @@
1
+ import { AdspConfiguration, EnvironmentName } from '@abgov/nx-oc';
2
+
3
+ export interface Schema {
4
+ name: string;
5
+ env: EnvironmentName;
6
+ accessToken?: string;
7
+ }
8
+
9
+ export interface NormalizedSchema extends Schema {
10
+ adsp: AdspConfiguration;
11
+ }
@@ -0,0 +1,26 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "cli": "nx",
4
+ "$id": "mean",
5
+ "title": "MEAN fullstack generator",
6
+ "type": "object",
7
+ "properties": {
8
+ "name": {
9
+ "type": "string",
10
+ "description": "Name of the project.",
11
+ "$default": { "$source": "argv", "index": 0 },
12
+ "x-prompt": "What name would you like to use for the project?"
13
+ },
14
+ "env": {
15
+ "type": "string",
16
+ "description": "ADSP environment to initialize the project for.",
17
+ "enum": ["dev", "test", "prod"],
18
+ "default": "prod"
19
+ },
20
+ "accessToken": {
21
+ "type": "string",
22
+ "description": "Access token for ADSP tenant configuration (skips interactive login)."
23
+ }
24
+ },
25
+ "required": ["name"]
26
+ }
@@ -0,0 +1,76 @@
1
+ # AGENTS.md — <%= projectName %>
2
+
3
+ React frontend for the Alberta Digital Service Platform (ADSP).
4
+ Generated by `nx g @abgov/nx-adsp:react-app`.
5
+
6
+ ## Stack
7
+
8
+ - **UI**: React 18 + GoA design system (`@abgov/react-components` — `Goab*` components)
9
+ - **Auth**: `keycloak-js` via Redux Toolkit slice (`src/app/user.slice.ts`)
10
+ - **State**: Redux Toolkit — store in `src/store.ts`
11
+ - **Router**: React Router v6
12
+
13
+ ## Key files
14
+
15
+ | File | Purpose |
16
+ |------|---------|
17
+ | `src/main.tsx` | Entry — Redux Provider, BrowserRouter, dispatches `initializeUser()` after render |
18
+ | `src/app/user.slice.ts` | Keycloak auth — login, logout, token refresh, `getAccessToken()` |
19
+ | `src/app/start.slice.ts` | Example public/private API calls |
20
+ | `src/app/config.slice.ts` | Loads ADSP directory service endpoints on startup |
21
+ | `src/app/intake.slice.ts` | Application domain state — extend this for your feature |
22
+ | `src/store.ts` | Redux store — add new slices here |
23
+ | `src/environments/environment.ts` | Access URL, realm, client ID — pre-set from ADSP tenant |
24
+
25
+ ## Auth pattern
26
+
27
+ ```typescript
28
+ import { getAccessToken, loginUser, logoutUser, userSelector } from './user.slice';
29
+
30
+ // In a component:
31
+ const { authenticated, name } = useSelector(userSelector);
32
+ dispatch(loginUser()); // redirects to Keycloak
33
+ dispatch(logoutUser()); // redirects to Keycloak logout
34
+
35
+ // In a thunk (for authenticated API calls):
36
+ const token = await getAccessToken(); // refreshes token if needed
37
+ ```
38
+
39
+ ## GoA design system
40
+
41
+ Components are imported from `@abgov/react-components` with the `Goab` prefix:
42
+
43
+ ```typescript
44
+ import { GoabButton, GoabAppHeader, GoabButtonGroup } from '@abgov/react-components';
45
+ ```
46
+
47
+ ## Adding a new Redux slice
48
+
49
+ 1. Create `src/app/my-feature.slice.ts` using `createSlice` / `createAsyncThunk`
50
+ 2. Export the reducer and add it to `src/store.ts`
51
+
52
+ ## Backend API calls (mern / proxy setup)
53
+
54
+ If `proxy.conf.json` exists at the project root, API calls are proxied to the
55
+ backend service in development. Use relative `/api/` paths — do not hardcode
56
+ the service URL:
57
+
58
+ ```typescript
59
+ // ✓ correct — works through proxy in dev, nginx in production
60
+ const response = await fetch('/api/v1/my-resource');
61
+
62
+ // ✗ wrong — bypasses proxy, won't work in production
63
+ const response = await fetch('http://localhost:3333/my-service/v1/my-resource');
64
+ ```
65
+
66
+ In production, `nginx.conf` contains the same proxy rule routing `/api/` to
67
+ the backend service hostname.
68
+
69
+ ## What NOT to change
70
+
71
+ - `user.slice.ts` — keycloak-js is initialised once per app lifecycle; do not
72
+ create a second Keycloak instance
73
+ - `environments/environment.ts` — access URL and realm are pre-configured for
74
+ the ADSP tenant; override at runtime via deployment config
75
+ - `silent-check-sso.html` — required for keycloak-js silent SSO; must be served
76
+ from the app root
@@ -1,47 +0,0 @@
1
- import { NgModule, APP_INITIALIZER, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
2
- import { BrowserModule } from '@angular/platform-browser';
3
- import "@abgov/web-components";
4
- import { AngularComponentsModule } from '@abgov/angular-components';
5
-
6
- import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
7
- import { AuthInterceptor } from './auth.interceptor';
8
- import { FormsModule } from '@angular/forms';
9
- import { AppRoutingModule } from './app.routes';
10
- import { ProtectedComponent } from './protected/protected.component';
11
- import { AuthCallbackComponent } from './auth-callback/auth-callback.component';
12
- import { AuthGuardService } from './services/auth-guard.service';
13
- import { AuthService } from './services/auth.service';
14
- import { AppComponent } from './app.component';
15
- import { Config } from '../environments/config';
16
-
17
- @NgModule({
18
- imports: [
19
- AngularComponentsModule,
20
- BrowserModule,
21
- HttpClientModule,
22
- FormsModule,
23
- AppRoutingModule,
24
- ],
25
- providers: [
26
- AuthGuardService,
27
- AuthService,
28
- Config,
29
- {
30
- provide: APP_INITIALIZER,
31
- useFactory: initializeApp,
32
- deps: [Config],
33
- multi: true,
34
- },
35
- { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true },
36
- ],
37
- declarations: [AppComponent, AuthCallbackComponent, ProtectedComponent],
38
- bootstrap: [AppComponent],
39
- schemas: [CUSTOM_ELEMENTS_SCHEMA],
40
- })
41
- export class AppModule {}
42
-
43
- export function initializeApp(config: Config) {
44
- return (): Promise<any> => {
45
- return config.Init();
46
- };
47
- }
@@ -1,13 +0,0 @@
1
- .info {
2
- display: flex;
3
- flex-direction: column;
4
- justify-content: center;
5
- }
6
-
7
- .token {
8
- font-family: monospace;
9
- background-color: beige;
10
- width: 600px;
11
- text-align: left;
12
- word-break: break-all;
13
- }
@@ -1,12 +0,0 @@
1
- <h3>Redirected to auth-callback route!</h3>
2
- <p>Now you are logged in and allowed to access the "protected" route...</p>
3
- <div class="info">
4
- <div>
5
- <div>Token Type:</div>
6
- <div class="token">{{ tokenType }}</div>
7
- </div>
8
- <div>
9
- <div>Token:</div>
10
- <div class="token">{{ accessToken }}</div>
11
- </div>
12
- </div>
@@ -1,33 +0,0 @@
1
- import { ComponentFixture, TestBed } from '@angular/core/testing';
2
- import { AuthCallbackComponent } from './auth-callback.component';
3
- import {
4
- BrowserDynamicTestingModule,
5
- platformBrowserDynamicTesting
6
- } from "@angular/platform-browser-dynamic/testing";
7
-
8
- describe('AuthCallbackComponent', () => {
9
- let component: AuthCallbackComponent;
10
- let fixture: ComponentFixture<AuthCallbackComponent>;
11
- let envData = {"production":false,"access":{"url":"https://testurl.com","realm":"123","client_id":"urn:ads:platform:tenant-admin-app"},"tenantApi":{"host":"http://localhost:3333","endpoints":{"tenantNameByRealm":"/api/tenant/v1/realm"}}}
12
-
13
- localStorage.setItem('envData', JSON.stringify(envData));
14
-
15
- beforeEach((() => {
16
- TestBed.initTestEnvironment(
17
- BrowserDynamicTestingModule,
18
- platformBrowserDynamicTesting()
19
- );
20
-
21
- TestBed.configureTestingModule({
22
- declarations: [AuthCallbackComponent],
23
- }).compileComponents();
24
-
25
- fixture = TestBed.createComponent(AuthCallbackComponent);
26
- component = fixture.componentInstance;
27
- fixture.detectChanges();
28
- }));
29
-
30
- it('should create', () => {
31
- expect(component).toBeTruthy();
32
- });
33
- });