@dvsa/appdev-api-common 0.3.2 → 0.4.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
@@ -35,3 +35,15 @@ There are two ways in which this package can/should be published:
35
35
  ###### Requires manual version bump via the PR
36
36
 
37
37
  - Upon merge into `main` branch, the package will be published via a GHA workflow.
38
+
39
+ ### Developing locally
40
+ To test our your changes before publishing to `npm`, you can use the following command:
41
+
42
+ `npm run localLink`
43
+
44
+ Then in the project you wish to use this package, run:
45
+
46
+ `npm link @dvsa/appdev-api-common`
47
+
48
+ Once you've completed your local testing and/or to start again from scratch, you can run:
49
+ `npm unlink @dvsa/appdev-api-common`
@@ -8,7 +8,8 @@ export declare class JWTAuthChecker {
8
8
  /**
9
9
  * Perform a JWT token verification and role check
10
10
  * @param {RoutingControllersRequest} request
11
- * @param roles
11
+ * @param {string | string[]} roles
12
+ * @returns {Promise<boolean>}
12
13
  */
13
14
  static execute({ request }: Action, roles?: string | string[]): Promise<boolean>;
14
15
  }
@@ -9,14 +9,17 @@ class JWTAuthChecker {
9
9
  /**
10
10
  * Perform a JWT token verification and role check
11
11
  * @param {RoutingControllersRequest} request
12
- * @param roles
12
+ * @param {string | string[]} roles
13
+ * @returns {Promise<boolean>}
13
14
  */
14
15
  static async execute({ request }, roles = []) {
15
16
  // if running locally, skip the token auth and role check
16
- if (process.env.IS_OFFLINE === "true" && process.env.FORCE_LOCAL_AUTH !== "true")
17
+ if (process.env.IS_OFFLINE === "true" &&
18
+ process.env.FORCE_LOCAL_AUTH !== "true")
17
19
  return true;
18
20
  // extract the token from the request headers
19
- const headers = request?.apiGateway.event.headers;
21
+ const headers = request?.apiGateway.event
22
+ .headers;
20
23
  const token = headers?.Authorization || headers?.authorization;
21
24
  // if no token is found, then deny access to resource
22
25
  if (!token || token.trim()?.length === 0) {
@@ -11,9 +11,32 @@ export declare class ClientCredentials {
11
11
  private readonly scope;
12
12
  private readonly debugMode;
13
13
  private static accessToken;
14
- private static readonly grant_type;
14
+ private static readonly grantType;
15
+ /**
16
+ * Create a new instance of the ClientCredentials class
17
+ * @param tokenUrl - The URL to fetch the access token from
18
+ * @param clientId - The client id
19
+ * @param clientSecret - The client secret
20
+ * @param scope - The scope of the access token
21
+ * @param debugMode - Whether to log debug messages
22
+ */
15
23
  constructor(tokenUrl: string, clientId: string, clientSecret: string, scope: string, debugMode?: boolean);
24
+ /**
25
+ * Helper method to perform the client credentials flow and return the access token
26
+ * This method will check for the existence of the token and if it is expired, it will fetch a new one
27
+ * @returns {Promise<string>} - The access token
28
+ */
16
29
  getAccessToken(): Promise<string>;
30
+ /**
31
+ * Fetch the client credentials from the token URL
32
+ * @returns {Promise<ClientCredentialsResponse>} - The response from the token URL
33
+ * @private
34
+ */
17
35
  private fetchClientCredentials;
36
+ /**
37
+ * Check if the access token is expired
38
+ * @returns {boolean} - Whether the access token is expired
39
+ * @private
40
+ */
18
41
  private isAccessTokenExpired;
19
42
  }
@@ -10,7 +10,15 @@ class ClientCredentials {
10
10
  scope;
11
11
  debugMode;
12
12
  static accessToken;
13
- static grant_type = "client_credentials";
13
+ static grantType = "client_credentials";
14
+ /**
15
+ * Create a new instance of the ClientCredentials class
16
+ * @param tokenUrl - The URL to fetch the access token from
17
+ * @param clientId - The client id
18
+ * @param clientSecret - The client secret
19
+ * @param scope - The scope of the access token
20
+ * @param debugMode - Whether to log debug messages
21
+ */
14
22
  constructor(tokenUrl, clientId, clientSecret, scope, debugMode = false) {
15
23
  this.tokenUrl = tokenUrl;
16
24
  this.clientId = clientId;
@@ -18,6 +26,11 @@ class ClientCredentials {
18
26
  this.scope = scope;
19
27
  this.debugMode = debugMode;
20
28
  }
29
+ /**
30
+ * Helper method to perform the client credentials flow and return the access token
31
+ * This method will check for the existence of the token and if it is expired, it will fetch a new one
32
+ * @returns {Promise<string>} - The access token
33
+ */
21
34
  async getAccessToken() {
22
35
  if (!ClientCredentials.accessToken || this.isAccessTokenExpired()) {
23
36
  const { access_token } = await this.fetchClientCredentials();
@@ -30,12 +43,17 @@ class ClientCredentials {
30
43
  }
31
44
  return ClientCredentials.accessToken;
32
45
  }
46
+ /**
47
+ * Fetch the client credentials from the token URL
48
+ * @returns {Promise<ClientCredentialsResponse>} - The response from the token URL
49
+ * @private
50
+ */
33
51
  async fetchClientCredentials() {
34
52
  const response = await fetch(this.tokenUrl, {
35
53
  method: "POST",
36
54
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
37
55
  body: (0, node_querystring_1.stringify)({
38
- grant_type: ClientCredentials.grant_type,
56
+ grant_type: ClientCredentials.grantType,
39
57
  client_id: this.clientId,
40
58
  client_secret: this.clientSecret,
41
59
  scope: this.scope,
@@ -47,6 +65,11 @@ class ClientCredentials {
47
65
  }
48
66
  return (await response.json());
49
67
  }
68
+ /**
69
+ * Check if the access token is expired
70
+ * @returns {boolean} - Whether the access token is expired
71
+ * @private
72
+ */
50
73
  isAccessTokenExpired() {
51
74
  try {
52
75
  const decodedAccessToken = (0, jose_1.decodeJwt)(ClientCredentials.accessToken);
@@ -5,9 +5,14 @@ export declare class JwtAuthoriser {
5
5
  private static readonly tokenExpiryEnvExclusionList;
6
6
  private static readonly JWKS_URI;
7
7
  private static JWKS;
8
+ /**
9
+ * Create a new instance of the JwtAuthoriser class
10
+ * @param clientId - the client id to validate the token against
11
+ */
8
12
  constructor(clientId?: string | null);
9
13
  /**
10
14
  * Validate a JWT and return the decoded payload
15
+ * @param {string} token - the JWT token to validate
11
16
  * @returns {Promise<JWTPayload>}
12
17
  */
13
18
  verify(token: string): Promise<JWTPayload>;
@@ -13,11 +13,16 @@ class JwtAuthoriser {
13
13
  ];
14
14
  static JWKS_URI = new URL("https://login.microsoftonline.com/common/discovery/keys");
15
15
  static JWKS = (0, jose_1.createRemoteJWKSet)(JwtAuthoriser.JWKS_URI);
16
+ /**
17
+ * Create a new instance of the JwtAuthoriser class
18
+ * @param clientId - the client id to validate the token against
19
+ */
16
20
  constructor(clientId = null) {
17
21
  this.clientId = clientId;
18
22
  }
19
23
  /**
20
24
  * Validate a JWT and return the decoded payload
25
+ * @param {string} token - the JWT token to validate
21
26
  * @returns {Promise<JWTPayload>}
22
27
  */
23
28
  async verify(token) {
package/index.d.ts CHANGED
@@ -3,4 +3,5 @@ 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/date-time";
6
7
  export * from "./validation/request-body";
package/index.js CHANGED
@@ -19,4 +19,5 @@ __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/date-time"), exports);
22
23
  __exportStar(require("./validation/request-body"), exports);
package/jest.config.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { Config } from "jest";
2
+
3
+ const config: Config = {
4
+ preset: "ts-jest",
5
+ testEnvironment: "node",
6
+ };
7
+
8
+ export default config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dvsa/appdev-api-common",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "keywords": [
5
5
  "dvsa",
6
6
  "nodejs",
@@ -13,18 +13,22 @@
13
13
  "access": "public"
14
14
  },
15
15
  "scripts": {
16
+ "localLink": "npm run clean && npm version patch && npm run build && cp package.json dist && cd dist && npm link",
16
17
  "clean": "rimraf coverage dist",
17
18
  "clean:temp": "rimraf auth api",
18
19
  "lint": "biome check src",
19
20
  "lint:fix": "npm run lint -- --write",
20
21
  "build": "npm run clean && tsc",
21
22
  "build:package": "npm run build",
23
+ "test": "jest",
24
+ "test:coverage": "jest --coverage",
22
25
  "prepublishOnly": "npm run build:package && cp -r ./dist/* . && rm -rf ./dist",
23
26
  "postpublish": "git clean -fd && npm run clean:temp",
24
27
  "gitSecrets": "git secrets --scan . && git log -p -- . | scanrepo"
25
28
  },
26
29
  "dependencies": {
27
30
  "ajv": "^8.17.1",
31
+ "dayjs": "^1.11.13",
28
32
  "jose": "^5.9.6"
29
33
  },
30
34
  "devDependencies": {
@@ -38,9 +42,12 @@
38
42
  "jest": "^29.7.0",
39
43
  "lint-staged": "^15.2.10",
40
44
  "rimraf": "^6.0.1",
41
- "routing-controllers": "^0.10.4",
45
+ "routing-controllers": "^0.11.2",
42
46
  "ts-jest": "^29.2.5",
43
47
  "ts-node": "^10.9.2",
44
48
  "typescript": "^5.5.2"
49
+ },
50
+ "lint-staged": {
51
+ "*.{js,ts,mjs,css,md,ts,json}": "npm run lint:fix -- --no-errors-on-unmatched"
45
52
  }
46
53
  }
@@ -0,0 +1,24 @@
1
+ import dayjs from "dayjs";
2
+ type AcceptableDate = DateTime | string | Date | null;
3
+ export declare class DateTime {
4
+ private instance;
5
+ private static readonly UKLocalDateTimeFormat;
6
+ private static readonly UKLocalDateFormat;
7
+ constructor(sourceDateTime?: AcceptableDate, format?: string | undefined);
8
+ static at(sourceDateTime: AcceptableDate, format?: string | undefined): DateTime | null;
9
+ static StandardUkLocalDateTimeAdapter(sourceDateTime: AcceptableDate): string | null;
10
+ static StandardUkLocalDateAdapter(sourceDateTime: AcceptableDate): string | null;
11
+ add(amount: number, unit: dayjs.ManipulateType): DateTime;
12
+ subtract(amount: number, unit: dayjs.ManipulateType): DateTime;
13
+ format(formatString: string): string;
14
+ day(): number;
15
+ toString(): string;
16
+ toISOString(): string;
17
+ isAfter(targetDate: AcceptableDate): boolean;
18
+ diff(targetDate: AcceptableDate, unit: dayjs.QUnitType, precise?: boolean): number;
19
+ daysDiff(targetDate: AcceptableDate): number;
20
+ compareDuration(targetDate: AcceptableDate, unit: dayjs.QUnitType): number;
21
+ isBefore(targetDate: AcceptableDate): boolean;
22
+ static today(): Date;
23
+ }
24
+ export {};
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DateTime = void 0;
7
+ const dayjs_1 = __importDefault(require("dayjs"));
8
+ const customParseFormat_1 = __importDefault(require("dayjs/plugin/customParseFormat"));
9
+ class DateTime {
10
+ instance;
11
+ static UKLocalDateTimeFormat = "DD/MM/YYYY HH:mm:ss";
12
+ static UKLocalDateFormat = "DD/MM/YYYY";
13
+ constructor(sourceDateTime, format = undefined) {
14
+ dayjs_1.default.extend(customParseFormat_1.default);
15
+ if (sourceDateTime === undefined || sourceDateTime === null) {
16
+ this.instance = (0, dayjs_1.default)();
17
+ }
18
+ else if (typeof sourceDateTime === "string" ||
19
+ sourceDateTime instanceof Date) {
20
+ this.instance = (0, dayjs_1.default)(sourceDateTime, format);
21
+ }
22
+ else {
23
+ this.instance = (0, dayjs_1.default)(sourceDateTime.instance, format);
24
+ }
25
+ }
26
+ static at(sourceDateTime, format = undefined) {
27
+ if (!sourceDateTime) {
28
+ return null;
29
+ }
30
+ return new DateTime(sourceDateTime, format);
31
+ }
32
+ static StandardUkLocalDateTimeAdapter(sourceDateTime) {
33
+ return (DateTime.at(sourceDateTime)?.format(DateTime.UKLocalDateTimeFormat) ||
34
+ null);
35
+ }
36
+ static StandardUkLocalDateAdapter(sourceDateTime) {
37
+ return (DateTime.at(sourceDateTime)?.format(DateTime.UKLocalDateFormat) || null);
38
+ }
39
+ add(amount, unit) {
40
+ this.instance = this.instance.add(amount, unit);
41
+ return this;
42
+ }
43
+ subtract(amount, unit) {
44
+ this.instance = this.instance.subtract(amount, unit);
45
+ return this;
46
+ }
47
+ format(formatString) {
48
+ return this.instance.format(formatString);
49
+ }
50
+ day() {
51
+ return this.instance.day();
52
+ }
53
+ toString() {
54
+ return this.instance.toString();
55
+ }
56
+ toISOString() {
57
+ return this.instance.toISOString();
58
+ }
59
+ isAfter(targetDate) {
60
+ const date = new DateTime(targetDate);
61
+ return this.instance.isAfter(date.instance);
62
+ }
63
+ diff(targetDate, unit, precise) {
64
+ const date = new DateTime(targetDate);
65
+ return this.instance.diff(date.instance, unit, precise);
66
+ }
67
+ daysDiff(targetDate) {
68
+ const date = new DateTime(targetDate);
69
+ return this.instance
70
+ .startOf("day")
71
+ .diff(date.instance.startOf("day"), "day");
72
+ }
73
+ compareDuration(targetDate, unit) {
74
+ const date = new DateTime(targetDate);
75
+ return date.instance.diff(this.instance, unit);
76
+ }
77
+ isBefore(targetDate) {
78
+ const date = new DateTime(targetDate);
79
+ return this.instance.isBefore(date.instance);
80
+ }
81
+ static today() {
82
+ return (0, dayjs_1.default)().toDate();
83
+ }
84
+ }
85
+ exports.DateTime = DateTime;
@@ -1,10 +1,13 @@
1
+ interface ValidateRequestBodyOptions {
2
+ isArray?: boolean;
3
+ errorDetails?: boolean;
4
+ }
1
5
  /**
2
6
  * Decorator tp validate an express request body against a specified schema
3
7
  * @param {object} schema - the json schema you wish to use as the validator
4
- * @param isArray - whether the body is expected to be an array
5
- * @param errorDetails - whether to return detailed error messages (Note: errors are logged regardless of this setting)
8
+ * @param {ValidateRequestBodyOptions} opts
9
+ * - isArray: whether the body is expected to be an array
10
+ * - errorDetails: whether to return detailed error messages (Note: errors are logged regardless of this setting)
6
11
  */
7
- export declare function ValidateRequestBody<T>(schema: object, { isArray, errorDetails }?: {
8
- isArray: boolean;
9
- errorDetails: boolean;
10
- }): (_target: T, _propertyKey: string, descriptor: PropertyDescriptor) => void;
12
+ export declare function ValidateRequestBody<T>(schema: object, opts?: ValidateRequestBodyOptions): (_target: T, _propertyKey: string, descriptor: PropertyDescriptor) => void;
13
+ export {};
@@ -11,10 +11,11 @@ ajv.addKeyword("tsEnumNames");
11
11
  /**
12
12
  * Decorator tp validate an express request body against a specified schema
13
13
  * @param {object} schema - the json schema you wish to use as the validator
14
- * @param isArray - whether the body is expected to be an array
15
- * @param errorDetails - whether to return detailed error messages (Note: errors are logged regardless of this setting)
14
+ * @param {ValidateRequestBodyOptions} opts
15
+ * - isArray: whether the body is expected to be an array
16
+ * - errorDetails: whether to return detailed error messages (Note: errors are logged regardless of this setting)
16
17
  */
17
- function ValidateRequestBody(schema, { isArray, errorDetails } = { isArray: false, errorDetails: false }) {
18
+ function ValidateRequestBody(schema, opts = { isArray: false, errorDetails: false }) {
18
19
  return (_target, _propertyKey, descriptor) => {
19
20
  const originalMethod = descriptor.value;
20
21
  descriptor.value = async function (body, res, next) {
@@ -27,8 +28,8 @@ function ValidateRequestBody(schema, { isArray, errorDetails } = { isArray: fals
27
28
  const payload = Buffer.isBuffer(body)
28
29
  ? JSON.parse(body.toString("utf-8"))
29
30
  : body;
30
- // Create the appropriate schema based on whether we're validating an array
31
- const schemaToValidate = isArray
31
+ // Create the appropriate schema based on whether we're validating an array or a single object
32
+ const schemaToValidate = opts?.isArray
32
33
  ? { type: "array", items: schema }
33
34
  : schema;
34
35
  const validateFunction = ajv.compile(schemaToValidate);
@@ -40,7 +41,7 @@ function ValidateRequestBody(schema, { isArray, errorDetails } = { isArray: fals
40
41
  const response = {
41
42
  message: "Validation error",
42
43
  };
43
- if (errorDetails) {
44
+ if (opts?.errorDetails) {
44
45
  Object.assign(response, { errors: validateFunction.errors });
45
46
  }
46
47
  return res.status(http_status_codes_1.HttpStatus.BAD_REQUEST).json(response);