@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 +12 -0
- package/auth/auth-checker.d.ts +2 -1
- package/auth/auth-checker.js +6 -3
- package/auth/client-credentials.d.ts +24 -1
- package/auth/client-credentials.js +25 -2
- package/auth/verify-jwt.d.ts +5 -0
- package/auth/verify-jwt.js +5 -0
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/jest.config.ts +8 -0
- package/package.json +9 -2
- package/utils/date-time.d.ts +24 -0
- package/utils/date-time.js +85 -0
- package/validation/request-body.d.ts +9 -6
- package/validation/request-body.js +7 -6
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`
|
package/auth/auth-checker.d.ts
CHANGED
|
@@ -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
|
}
|
package/auth/auth-checker.js
CHANGED
|
@@ -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" &&
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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);
|
package/auth/verify-jwt.d.ts
CHANGED
|
@@ -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>;
|
package/auth/verify-jwt.js
CHANGED
|
@@ -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
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dvsa/appdev-api-common",
|
|
3
|
-
"version": "0.
|
|
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.
|
|
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
|
|
5
|
-
*
|
|
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,
|
|
8
|
-
|
|
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
|
|
15
|
-
*
|
|
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,
|
|
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);
|