@dvsa/appdev-api-common 0.4.4 → 0.5.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/README.md +289 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/package.json +1 -1
- package/utils/compression.d.ts +16 -0
- package/utils/compression.js +30 -0
- package/validation/request-body.js +0 -2
- package/validation/validation-error.d.ts +6 -2
- package/validation/validation-error.js +15 -1
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Common code used by the various serverless microservices withing AppDev systems, published as a NPM package.
|
|
4
4
|
|
|
5
|
+
## Developing
|
|
5
6
|
### Pre-requisites
|
|
6
7
|
|
|
7
8
|
- Node.js (Please see `.nvmrc` for specific version)
|
|
@@ -47,3 +48,291 @@ Then in the project you wish to use this package, run:
|
|
|
47
48
|
|
|
48
49
|
Once you've completed your local testing and/or to start again from scratch, you can run:
|
|
49
50
|
`npm unlink @dvsa/appdev-api-common`
|
|
51
|
+
|
|
52
|
+
# Contents
|
|
53
|
+
|
|
54
|
+
## JWTAuthChecker
|
|
55
|
+
|
|
56
|
+
### Overview
|
|
57
|
+
`JWTAuthChecker` is a utility class for verifying JSON Web Tokens (JWT) and enforcing role-based access control (RBAC) in an Express application using `routing-controllers`.
|
|
58
|
+
|
|
59
|
+
It performs authentication by extracting the JWT from the request headers, verifying its validity, and checking whether the user has the required roles to access a resource.
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
### Example Usage in a Controller
|
|
64
|
+
The `JWTAuthChecker.execute` method can be used in conjunction with the `@Authorized` decorator to enforce authentication and role-based access.
|
|
65
|
+
|
|
66
|
+
#### Basic Authentication Check
|
|
67
|
+
In the entry point to your service/application e.g. `src/index.ts`, bind the `JWTAuthChecker.execute` method to the `authorizationChecker` option in the `createExpressServer` function.
|
|
68
|
+
```ts
|
|
69
|
+
// ...other imports
|
|
70
|
+
import { JWTAuthChecker } from '@dvsa/appdev-api-common';
|
|
71
|
+
import { MyResource } from '@resources/MyResource';
|
|
72
|
+
import { createExpressServer } from 'routing-controllers';
|
|
73
|
+
|
|
74
|
+
export const app = createExpressServer({
|
|
75
|
+
cors: true,
|
|
76
|
+
defaultErrorHandler: false,
|
|
77
|
+
controllers: [MyResource],
|
|
78
|
+
authorizationChecker: JWTAuthChecker.execute,
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
If no roles are required, the function will simply verify the JWT token.
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import { Authorized, Get, JsonController } from "routing-controllers";
|
|
87
|
+
|
|
88
|
+
@JsonController("/example")
|
|
89
|
+
export class ExampleController {
|
|
90
|
+
@Authorized()
|
|
91
|
+
@Get("/secure-endpoint")
|
|
92
|
+
secureEndpoint() {
|
|
93
|
+
return { message: "Access granted" };
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
#### Role-Based Access Control
|
|
99
|
+
If specific roles are required, they can be passed as an argument.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
@JsonController("/admin")
|
|
103
|
+
export class AdminController {
|
|
104
|
+
@Authorized(["admin"])
|
|
105
|
+
@Get("/dashboard")
|
|
106
|
+
getAdminDashboard() {
|
|
107
|
+
return { message: "Admin access granted" };
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Manually Checking JWT Authentication
|
|
113
|
+
The `execute` method can also be called manually within custom middleware or service logic.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
import { Action } from "routing-controllers";
|
|
117
|
+
|
|
118
|
+
async function checkUserAuthorization(action: Action) {
|
|
119
|
+
try {
|
|
120
|
+
const isAuthorized = await JWTAuthChecker.execute(action, ["editor"]);
|
|
121
|
+
if (isAuthorized) {
|
|
122
|
+
console.log("User is authorized");
|
|
123
|
+
}
|
|
124
|
+
} catch (error) {
|
|
125
|
+
console.error("Authorization failed", error);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Environment Variables
|
|
131
|
+
The behavior of the authentication check can be controlled using environment variables:
|
|
132
|
+
|
|
133
|
+
- `IS_OFFLINE`: If set to `true`, the authentication check is bypassed (useful for local development).
|
|
134
|
+
- `FORCE_LOCAL_AUTH`: If set to `true`, authentication is enforced even in offline mode.
|
|
135
|
+
|
|
136
|
+
Example `.env` file:
|
|
137
|
+
```sh
|
|
138
|
+
IS_OFFLINE=true
|
|
139
|
+
FORCE_LOCAL_AUTH=false
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Error Handling
|
|
143
|
+
`JWTAuthChecker` throws an `AuthError` in case of authentication or authorization failures. The errors are structured with an HTTP status code and message.
|
|
144
|
+
|
|
145
|
+
Possible errors:
|
|
146
|
+
- **Missing Authorization header**: No token found in the request.
|
|
147
|
+
- **No roles found in token**: The JWT does not contain any roles.
|
|
148
|
+
- **Insufficient permissions**: The user does not have the required role(s).
|
|
149
|
+
|
|
150
|
+
Example error response:
|
|
151
|
+
```json
|
|
152
|
+
{
|
|
153
|
+
"status": 401,
|
|
154
|
+
"message": "Insufficient permissions",
|
|
155
|
+
"code": "UNAUTHORIZED"
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
# ClientCredentials
|
|
160
|
+
|
|
161
|
+
## Overview
|
|
162
|
+
`ClientCredentials` is a utility class for handling OAuth2 client credentials authentication. It fetches and manages an access token from an authorization server, caching it for reuse until it expires.
|
|
163
|
+
|
|
164
|
+
This implementation helps applications authenticate machine-to-machine (M2M) interactions by using client credentials to obtain a bearer token.
|
|
165
|
+
|
|
166
|
+
## Usage
|
|
167
|
+
|
|
168
|
+
### Importing the `ClientCredentials` Class
|
|
169
|
+
```ts
|
|
170
|
+
import { ClientCredentials } from '@dvsa/appdev-api-common';
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Initializing the ClientCredentials Instance
|
|
174
|
+
```ts
|
|
175
|
+
const clientCredentials = new ClientCredentials(
|
|
176
|
+
"https://auth.example.com/token", // Token URL
|
|
177
|
+
"your-client-id", // Client ID
|
|
178
|
+
"your-client-secret", // Client Secret
|
|
179
|
+
"your-scope", // OAuth2 Scope
|
|
180
|
+
true // Debug mode (optional)
|
|
181
|
+
);
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Retrieving an Access Token
|
|
185
|
+
To obtain an access token, call the `getAccessToken` method. This method caches the token and only fetches a new one if the current token is expired or unavailable.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
async function authenticate() {
|
|
189
|
+
try {
|
|
190
|
+
const accessToken = await clientCredentials.getAccessToken();
|
|
191
|
+
console.log("Access Token:", accessToken);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.error("Failed to retrieve access token", error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
authenticate();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Debugging Mode
|
|
201
|
+
If `debugMode` is enabled (set to `true` during instantiation), the class will log debug messages indicating whether it is fetching a new token or using a cached one.
|
|
202
|
+
|
|
203
|
+
Example logs:
|
|
204
|
+
```sh
|
|
205
|
+
[DEBUG] New access token fetched: eyJhbGciOi...
|
|
206
|
+
[DEBUG] Using existing access token: eyJhbGciOi...
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Error Handling
|
|
210
|
+
`ClientCredentials` throws an error if token retrieval fails. Ensure your application catches these errors to prevent failures in authentication-dependent workflows.
|
|
211
|
+
|
|
212
|
+
Possible errors:
|
|
213
|
+
- **Failed to fetch client credentials**: Occurs when the token endpoint returns a non-OK response.
|
|
214
|
+
- **Error decoding access token**: Happens when the received JWT is invalid or cannot be parsed.
|
|
215
|
+
|
|
216
|
+
Example error handling:
|
|
217
|
+
```ts
|
|
218
|
+
try {
|
|
219
|
+
const token = await clientCredentials.getAccessToken();
|
|
220
|
+
} catch (error) {
|
|
221
|
+
console.error("Authentication error:", error);
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
# DateTime
|
|
226
|
+
|
|
227
|
+
## Overview
|
|
228
|
+
`DateTime` is a utility class built on `dayjs` to provide a structured and flexible way to handle date and time operations. It supports parsing, formatting, arithmetic operations, and comparisons.
|
|
229
|
+
|
|
230
|
+
## Usage
|
|
231
|
+
|
|
232
|
+
### Importing the `DateTime` Class
|
|
233
|
+
```ts
|
|
234
|
+
import { DateTime } from '@dvsa/appdev-api-common';
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Creating a `DateTime` Instance
|
|
238
|
+
You can create an instance of `DateTime` using a `Date`, `string`, or another `DateTime` instance.
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
const now = new DateTime(); // Current date and time
|
|
242
|
+
const fromString = new DateTime("15/03/2025", "DD/MM/YYYY");
|
|
243
|
+
const fromDate = new DateTime(new Date());
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Formatting Dates
|
|
247
|
+
You can format a `DateTime` instance using the `format` method.
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
console.log(now.format("YYYY-MM-DD HH:mm:ss"));
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Standard UK Local Date Formats
|
|
254
|
+
The class provides helper methods for formatting dates in UK local formats:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
console.log(DateTime.StandardUkLocalDateTimeAdapter(now)); // "DD/MM/YYYY HH:mm:ss"
|
|
258
|
+
console.log(DateTime.StandardUkLocalDateAdapter(now)); // "DD/MM/YYYY"
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Arithmetic Operations
|
|
262
|
+
You can add or subtract time units to/from a `DateTime` instance.
|
|
263
|
+
|
|
264
|
+
```ts
|
|
265
|
+
const futureDate = now.add(5, "days");
|
|
266
|
+
const pastDate = now.subtract(2, "weeks");
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Date Comparisons
|
|
270
|
+
```ts
|
|
271
|
+
const date1 = new DateTime("2025-03-10");
|
|
272
|
+
const date2 = new DateTime("2025-03-15");
|
|
273
|
+
|
|
274
|
+
console.log(date1.isBefore(date2)); // true
|
|
275
|
+
console.log(date2.isAfter(date1)); // true
|
|
276
|
+
console.log(date1.isBetween("2025-03-05", "2025-03-20")); // true
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Difference Between Dates
|
|
280
|
+
Get the difference in various units:
|
|
281
|
+
|
|
282
|
+
```ts
|
|
283
|
+
const daysDiff = date1.daysDiff(date2); // Number of whole days between dates
|
|
284
|
+
const hoursDiff = date1.diff(date2, "hour");
|
|
285
|
+
console.log(`Difference: ${daysDiff} days, ${hoursDiff} hours`);
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Comparing Durations
|
|
289
|
+
```ts
|
|
290
|
+
const duration = date1.compareDuration(date2, "minute");
|
|
291
|
+
console.log(`Difference in minutes: ${duration}`);
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Getting the Current Date
|
|
295
|
+
```ts
|
|
296
|
+
const today = DateTime.today();
|
|
297
|
+
console.log(today);
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## Error Handling
|
|
301
|
+
Ensure that input dates are in a valid format when creating a `DateTime` instance. If an invalid format is provided, `dayjs` will handle parsing failures gracefully but may return an invalid instance.
|
|
302
|
+
|
|
303
|
+
Example error handling:
|
|
304
|
+
```ts
|
|
305
|
+
const invalidDate = new DateTime("invalid-date");
|
|
306
|
+
console.log(invalidDate.toString()); // Returns an invalid date string
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# Compression
|
|
311
|
+
|
|
312
|
+
## Overview
|
|
313
|
+
`DataCompression` is a utility class to simplify the compression & decompression using Gzip and Gunzip algorithms.
|
|
314
|
+
|
|
315
|
+
## Usage
|
|
316
|
+
|
|
317
|
+
### Importing the `DataCompression` Class
|
|
318
|
+
```ts
|
|
319
|
+
import { DataCompression } from '@dvsa/appdev-api-common';
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Compressing Data
|
|
323
|
+
This is the process of taking a plain JSON object and compressing it using Gzip.
|
|
324
|
+
|
|
325
|
+
```ts
|
|
326
|
+
const data = { key: 'value' };
|
|
327
|
+
const compressedData = DataCompression.compress(data);
|
|
328
|
+
// H4sIAAAAAAAAA6tWyk6tVLJSKkvMKU1VqgUAv5wYPw8AAAA=
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Decompressing Data
|
|
332
|
+
This is the process of taking a compressed JSON object and decompressing it using Gunzip.
|
|
333
|
+
|
|
334
|
+
```ts
|
|
335
|
+
const data = "H4sIAAAAAAAAA6tWyk6tVLJSKkvMKU1VqgUAv5wYPw8AAAA=";
|
|
336
|
+
const decompressedData = DataCompression.decompress(data);
|
|
337
|
+
// { key: 'value' }
|
|
338
|
+
```
|
package/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export * from "./auth/auth-checker";
|
|
|
3
3
|
export * from "./auth/auth-errors";
|
|
4
4
|
export * from "./auth/client-credentials";
|
|
5
5
|
export * from "./auth/verify-jwt";
|
|
6
|
+
export * from "./utils/compression";
|
|
6
7
|
export * from "./utils/date-time";
|
|
7
8
|
export * from "./validation/request-body";
|
package/index.js
CHANGED
|
@@ -19,5 +19,6 @@ __exportStar(require("./auth/auth-checker"), exports);
|
|
|
19
19
|
__exportStar(require("./auth/auth-errors"), exports);
|
|
20
20
|
__exportStar(require("./auth/client-credentials"), exports);
|
|
21
21
|
__exportStar(require("./auth/verify-jwt"), exports);
|
|
22
|
+
__exportStar(require("./utils/compression"), exports);
|
|
22
23
|
__exportStar(require("./utils/date-time"), exports);
|
|
23
24
|
__exportStar(require("./validation/request-body"), exports);
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare class DataCompression {
|
|
2
|
+
/**
|
|
3
|
+
* Extracts a compressed (in base64) string using gunzip into a JSON object
|
|
4
|
+
* @template T
|
|
5
|
+
* @param {string} compressedData
|
|
6
|
+
* @returns {T}
|
|
7
|
+
*/
|
|
8
|
+
static decompress<T>(compressedData: string): T;
|
|
9
|
+
/**
|
|
10
|
+
* Compresses (with gzip) a JSON object into a base64 string
|
|
11
|
+
* @template T
|
|
12
|
+
* @param {T} data
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
static compress<T>(data: T): string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DataCompression = void 0;
|
|
4
|
+
const node_zlib_1 = require("node:zlib");
|
|
5
|
+
// biome-ignore lint/complexity/noStaticOnlyClass: Valid use case for a static class
|
|
6
|
+
class DataCompression {
|
|
7
|
+
/**
|
|
8
|
+
* Extracts a compressed (in base64) string using gunzip into a JSON object
|
|
9
|
+
* @template T
|
|
10
|
+
* @param {string} compressedData
|
|
11
|
+
* @returns {T}
|
|
12
|
+
*/
|
|
13
|
+
static decompress(compressedData) {
|
|
14
|
+
const gzippedBytes = Buffer.from(compressedData, "base64");
|
|
15
|
+
const unzippedJson = (0, node_zlib_1.gunzipSync)(gzippedBytes).toString();
|
|
16
|
+
return JSON.parse(unzippedJson);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Compresses (with gzip) a JSON object into a base64 string
|
|
20
|
+
* @template T
|
|
21
|
+
* @param {T} data
|
|
22
|
+
* @returns {string}
|
|
23
|
+
*/
|
|
24
|
+
static compress(data) {
|
|
25
|
+
const jsonString = JSON.stringify(data);
|
|
26
|
+
const gzippedData = (0, node_zlib_1.gzipSync)(Buffer.from(jsonString));
|
|
27
|
+
return gzippedData.toString("base64");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.DataCompression = DataCompression;
|
|
@@ -40,8 +40,6 @@ function ValidateRequestBody(schema, opts = { isArray: false, errorDetails: true
|
|
|
40
40
|
const isValid = validateFunction(payload);
|
|
41
41
|
// if an error exists, then return a 400 with details
|
|
42
42
|
if (!isValid) {
|
|
43
|
-
console.error("Validation failed on body:", JSON.stringify(body));
|
|
44
|
-
console.error(validateFunction.errors);
|
|
45
43
|
throw new validation_error_1.ValidationError(http_status_codes_1.HttpStatus.BAD_REQUEST, "Validation failed", opts?.errorDetails ? validateFunction.errors : null);
|
|
46
44
|
}
|
|
47
45
|
// proceed with attached method if schema is valid
|
|
@@ -1,14 +1,18 @@
|
|
|
1
|
+
import type { ErrorObject } from "ajv";
|
|
1
2
|
import { HttpStatus } from "../api/http-status-codes";
|
|
3
|
+
type ValidationErrorDetails = Partial<ErrorObject[]> | string | null;
|
|
2
4
|
export declare class ValidationError extends Error {
|
|
3
5
|
private readonly statusCode;
|
|
4
6
|
private readonly details;
|
|
5
|
-
constructor(statusCode?: HttpStatus, message?: string, details?:
|
|
7
|
+
constructor(statusCode?: HttpStatus, message?: string, details?: ValidationErrorDetails);
|
|
6
8
|
toJSON(): {
|
|
7
9
|
error: {
|
|
8
10
|
name: string;
|
|
9
11
|
message: string;
|
|
10
|
-
details:
|
|
12
|
+
details: ValidationErrorDetails;
|
|
11
13
|
statusCode: number;
|
|
12
14
|
};
|
|
13
15
|
};
|
|
16
|
+
private unwrapErrorDetails;
|
|
14
17
|
}
|
|
18
|
+
export {};
|
|
@@ -19,10 +19,24 @@ class ValidationError extends Error {
|
|
|
19
19
|
error: {
|
|
20
20
|
name: this.name,
|
|
21
21
|
message: this.message,
|
|
22
|
-
details: this.details,
|
|
22
|
+
details: this.unwrapErrorDetails(this.details),
|
|
23
23
|
statusCode: this.statusCode,
|
|
24
24
|
},
|
|
25
25
|
};
|
|
26
26
|
}
|
|
27
|
+
unwrapErrorDetails = (details) => {
|
|
28
|
+
if (Array.isArray(details)) {
|
|
29
|
+
return details.map((error) => {
|
|
30
|
+
if (error &&
|
|
31
|
+
typeof error === "object" &&
|
|
32
|
+
"keyword" in error &&
|
|
33
|
+
error.keyword === "enum") {
|
|
34
|
+
return `${error.instancePath.replace("/", "")} ${error.message}: ${error.params?.allowedValues.join(", ")}`;
|
|
35
|
+
}
|
|
36
|
+
return error?.message || error;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return details;
|
|
40
|
+
};
|
|
27
41
|
}
|
|
28
42
|
exports.ValidationError = ValidationError;
|