@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dvsa/appdev-api-common",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "keywords": ["dvsa", "nodejs", "typescript"],
5
5
  "author": "DVSA",
6
6
  "description": "Utils library for common API functionality",
@@ -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?: unknown | null);
7
+ constructor(statusCode?: HttpStatus, message?: string, details?: ValidationErrorDetails);
6
8
  toJSON(): {
7
9
  error: {
8
10
  name: string;
9
11
  message: string;
10
- details: unknown;
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;