@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.
- package/generators.json +6 -0
- package/package.json +1 -1
- package/src/generators/angular-app/angular-app.js +22 -7
- package/src/generators/angular-app/angular-app.js.map +1 -1
- package/src/generators/angular-app/files/AGENTS.md__tmpl__ +96 -0
- package/src/generators/angular-app/files/src/app/app.component.css__tmpl__ +10 -54
- package/src/generators/angular-app/files/src/app/app.component.html__tmpl__ +18 -49
- package/src/generators/angular-app/files/src/app/app.component.spec.ts__tmpl__ +22 -27
- package/src/generators/angular-app/files/src/app/app.component.ts__tmpl__ +60 -20
- package/src/generators/angular-app/files/src/app/app.config.ts__tmpl__ +27 -0
- package/src/generators/angular-app/files/src/app/app.routes.ts__tmpl__ +10 -31
- package/src/generators/angular-app/files/src/app/home/home.component.html__tmpl__ +1 -3
- package/src/generators/angular-app/files/src/app/home/home.component.ts__tmpl__ +9 -23
- package/src/generators/angular-app/files/src/app/protected/protected.component.html__tmpl__ +2 -2
- package/src/generators/angular-app/files/src/app/protected/protected.component.spec.ts__tmpl__ +27 -43
- package/src/generators/angular-app/files/src/app/protected/protected.component.ts__tmpl__ +15 -15
- package/src/generators/angular-app/files/src/app/services/auth-guard.service.ts__tmpl__ +12 -21
- package/src/generators/angular-app/files/src/environments/environment.ts__tmpl__ +2 -11
- package/src/generators/angular-app/files/src/index.html__tmpl__ +3 -9
- package/src/generators/angular-app/files/src/main.ts__tmpl__ +8 -12
- package/src/generators/angular-app/files/src/silent-check-sso.html__tmpl__ +5 -0
- package/src/generators/angular-app/files/src/styles.css__tmpl__ +2 -2
- package/src/generators/express-service/files/AGENTS.md__tmpl__ +76 -0
- package/src/generators/mean/mean.d.ts +3 -0
- package/src/generators/mean/mean.js +30 -0
- package/src/generators/mean/mean.js.map +1 -0
- package/src/generators/mean/mean.spec.ts +38 -0
- package/src/generators/mean/schema.d.ts +11 -0
- package/src/generators/mean/schema.json +26 -0
- package/src/generators/react-app/files/AGENTS.md__tmpl__ +76 -0
- package/src/generators/angular-app/files/src/app/app.module.ts__tmpl__ +0 -47
- package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.css__tmpl__ +0 -13
- package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.html__tmpl__ +0 -12
- package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.spec.ts__tmpl__ +0 -33
- package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.ts__tmpl__ +0 -48
- package/src/generators/angular-app/files/src/app/auth.interceptor.ts__tmpl__ +0 -24
- package/src/generators/angular-app/files/src/app/logout/logout.component.html__tmpl__ +0 -1
- package/src/generators/angular-app/files/src/app/logout/logout.component.ts__tmpl__ +0 -9
- package/src/generators/angular-app/files/src/app/services/auth.service.ts__tmpl__ +0 -57
- package/src/generators/angular-app/files/src/app/tenant.service.ts__tmpl__ +0 -19
- package/src/generators/angular-app/files/src/environments/config.ts__tmpl__ +0 -21
- /package/src/generators/angular-app/files/{src → public}/assets/banner.jpg +0 -0
- /package/src/generators/angular-app/files/{src → public}/assets/github-1.svg +0 -0
- /package/src/generators/angular-app/files/{src → public}/favicon.ico +0 -0
package/src/generators/angular-app/files/src/app/protected/protected.component.spec.ts__tmpl__
CHANGED
|
@@ -1,52 +1,36 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
28
|
-
})
|
|
29
|
-
);
|
|
28
|
+
afterEach(() => httpTesting.verify());
|
|
30
29
|
|
|
31
|
-
|
|
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
|
|
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: '
|
|
6
|
+
selector: '<%= projectName %>-protected',
|
|
7
|
+
standalone: true,
|
|
6
8
|
templateUrl: './protected.component.html',
|
|
7
|
-
|
|
9
|
+
styleUrl: './protected.component.css',
|
|
8
10
|
})
|
|
9
11
|
export class ProtectedComponent implements OnInit {
|
|
10
|
-
|
|
12
|
+
private http = inject(HttpClient);
|
|
13
|
+
private keycloak = inject(Keycloak);
|
|
11
14
|
|
|
12
|
-
|
|
15
|
+
privateMessage = signal('Not retrieved');
|
|
13
16
|
|
|
14
|
-
|
|
15
|
-
this.
|
|
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.
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
@
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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 %>-
|
|
13
|
+
<<%= projectName %>-root></<%= projectName %>-root>
|
|
20
14
|
</body>
|
|
21
15
|
</html>
|
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
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
|
-
|
|
5
|
-
|
|
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,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,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,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
|
-
}
|
package/src/generators/angular-app/files/src/app/auth-callback/auth-callback.component.html__tmpl__
DELETED
|
@@ -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
|
-
});
|