@dwp/govuk-casa 8.0.0-alpha2 → 8.0.1
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/CHANGELOG.md +14 -0
- package/README.md +1 -1
- package/dist/assets/css/casa-ie8.css +1 -1
- package/dist/assets/css/casa.css +1 -1
- package/dist/casa.d.ts +2 -1
- package/dist/casa.js +3 -1
- package/dist/lib/CasaTemplateLoader.d.ts +4 -2
- package/dist/lib/CasaTemplateLoader.js +26 -4
- package/dist/lib/JourneyContext.d.ts +38 -6
- package/dist/lib/JourneyContext.js +58 -17
- package/dist/lib/MutableRouter.js +6 -2
- package/dist/lib/Plan.d.ts +37 -4
- package/dist/lib/Plan.js +75 -11
- package/dist/lib/ValidationError.d.ts +6 -2
- package/dist/lib/ValidationError.js +7 -0
- package/dist/lib/ValidatorFactory.d.ts +72 -19
- package/dist/lib/ValidatorFactory.js +33 -20
- package/dist/lib/configuration-ingestor.d.ts +262 -0
- package/dist/lib/configuration-ingestor.js +464 -0
- package/dist/lib/configure.d.ts +26 -140
- package/dist/lib/configure.js +17 -45
- package/dist/lib/dirname.cjs +1 -1
- package/dist/lib/dirname.d.cts +2 -0
- package/dist/lib/end-session.d.ts +2 -1
- package/dist/lib/end-session.js +27 -7
- package/dist/lib/field.d.ts +39 -46
- package/dist/lib/field.js +75 -36
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.js +54 -0
- package/dist/lib/logger.d.ts +2 -1
- package/dist/lib/logger.js +3 -4
- package/dist/lib/nunjucks-filters.js +8 -0
- package/dist/lib/utils.d.ts +18 -2
- package/dist/lib/utils.js +56 -2
- package/dist/lib/validators/inArray.js +1 -1
- package/dist/lib/validators/index.js +0 -22
- package/dist/lib/validators/postalAddressObject.js +6 -2
- package/dist/lib/waypoint-url.d.ts +2 -1
- package/dist/lib/waypoint-url.js +3 -0
- package/dist/middleware/body-parser.d.ts +1 -0
- package/dist/middleware/body-parser.js +18 -9
- package/dist/middleware/data.d.ts +1 -2
- package/dist/middleware/data.js +9 -9
- package/dist/middleware/dirname.cjs +1 -1
- package/dist/middleware/dirname.d.cts +2 -0
- package/dist/middleware/gather-fields.d.ts +2 -1
- package/dist/middleware/gather-fields.js +6 -5
- package/dist/middleware/i18n.js +5 -1
- package/dist/middleware/post.js +6 -6
- package/dist/middleware/progress-journey.js +1 -1
- package/dist/middleware/sanitise-fields.js +9 -9
- package/dist/middleware/session.d.ts +2 -1
- package/dist/middleware/session.js +62 -55
- package/dist/middleware/skip-waypoint.js +2 -2
- package/dist/middleware/steer-journey.d.ts +2 -1
- package/dist/middleware/steer-journey.js +3 -0
- package/dist/middleware/validate-fields.js +7 -6
- package/dist/mjs/esm-wrapper.js +10 -0
- package/dist/routes/ancillary.d.ts +8 -1
- package/dist/routes/ancillary.js +7 -2
- package/dist/routes/dirname.cjs +1 -1
- package/dist/routes/dirname.d.cts +2 -0
- package/dist/routes/journey.js +14 -8
- package/dist/routes/static.js +4 -3
- package/package.json +42 -25
- package/views/casa/layouts/main.njk +2 -2
package/dist/lib/utils.d.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {import('./configuration-ingestor').GlobalHook} GlobalHook
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('./configuration-ingestor').PageHook} PageHook
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {GlobalHook | PageHook} Hook
|
|
9
|
+
*/
|
|
1
10
|
/**
|
|
2
11
|
* Test is a value can be stringifed (numbers or strings)
|
|
3
12
|
*
|
|
@@ -27,6 +36,13 @@ export function isEmpty(val: any): boolean;
|
|
|
27
36
|
* @param {string} hookName Hook name (including scope prefix)
|
|
28
37
|
* @param {string} path URL path to match (relative to mountUrl)
|
|
29
38
|
* @param {Hook[]} hooks Hooks to be applied at the page level
|
|
30
|
-
* @returns {
|
|
39
|
+
* @returns {Function[]} An array of middleware that should be applied
|
|
31
40
|
*/
|
|
32
|
-
export function resolveMiddlewareHooks(hookName: string, path: string, hooks?:
|
|
41
|
+
export function resolveMiddlewareHooks(hookName: string, path: string, hooks?: Hook[]): Function[];
|
|
42
|
+
export function validateWaypoint(waypoint: any): void;
|
|
43
|
+
export function validateView(view: any): void;
|
|
44
|
+
export function validateHookName(hookName: any): void;
|
|
45
|
+
export function validateHookPath(path: any): void;
|
|
46
|
+
export type GlobalHook = import('./configuration-ingestor').GlobalHook;
|
|
47
|
+
export type PageHook = import('./configuration-ingestor').PageHook;
|
|
48
|
+
export type Hook = GlobalHook | PageHook;
|
package/dist/lib/utils.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {import('./configuration-ingestor').GlobalHook} GlobalHook
|
|
4
|
+
*/
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resolveMiddlewareHooks = exports.isEmpty = exports.stringifyInput = exports.isStringable = void 0;
|
|
6
|
+
exports.validateHookPath = exports.validateHookName = exports.validateView = exports.validateWaypoint = exports.resolveMiddlewareHooks = exports.isEmpty = exports.stringifyInput = exports.isStringable = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {import('./configuration-ingestor').PageHook} PageHook
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {GlobalHook | PageHook} Hook
|
|
12
|
+
*/
|
|
4
13
|
/**
|
|
5
14
|
* Test is a value can be stringifed (numbers or strings)
|
|
6
15
|
*
|
|
@@ -37,6 +46,8 @@ function isEmpty(val) {
|
|
|
37
46
|
return true;
|
|
38
47
|
}
|
|
39
48
|
if (Array.isArray(val) || typeof val === 'object') {
|
|
49
|
+
// ESLint disabled as `k` is an "own property" (thanks to `Object.keys()`)
|
|
50
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
40
51
|
return Object.keys(val).filter((k) => !isEmpty(val[k])).length === 0;
|
|
41
52
|
}
|
|
42
53
|
return false;
|
|
@@ -49,7 +60,7 @@ exports.isEmpty = isEmpty;
|
|
|
49
60
|
* @param {string} hookName Hook name (including scope prefix)
|
|
50
61
|
* @param {string} path URL path to match (relative to mountUrl)
|
|
51
62
|
* @param {Hook[]} hooks Hooks to be applied at the page level
|
|
52
|
-
* @returns {
|
|
63
|
+
* @returns {Function[]} An array of middleware that should be applied
|
|
53
64
|
*/
|
|
54
65
|
function resolveMiddlewareHooks(hookName, path, hooks = []) {
|
|
55
66
|
/* eslint-disable-next-line max-len */
|
|
@@ -57,3 +68,46 @@ function resolveMiddlewareHooks(hookName, path, hooks = []) {
|
|
|
57
68
|
return hooks.filter((h) => h.hook === hookName).filter(pathMatch).map((h) => h.middleware);
|
|
58
69
|
}
|
|
59
70
|
exports.resolveMiddlewareHooks = resolveMiddlewareHooks;
|
|
71
|
+
/* ------------------------------------------------ validation / sanitisation */
|
|
72
|
+
function validateWaypoint(waypoint) {
|
|
73
|
+
if (typeof waypoint !== 'string') {
|
|
74
|
+
throw new TypeError('Waypoint must be a string');
|
|
75
|
+
}
|
|
76
|
+
if (!waypoint.length) {
|
|
77
|
+
throw new SyntaxError('Waypoint must not be empty');
|
|
78
|
+
}
|
|
79
|
+
if (waypoint.match(/[^/a-z0-9_-]/)) {
|
|
80
|
+
throw new SyntaxError('Waypoint must contain only a-z, 0-9, -, _ and / characters');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.validateWaypoint = validateWaypoint;
|
|
84
|
+
function validateView(view) {
|
|
85
|
+
if (typeof view !== 'string') {
|
|
86
|
+
throw new TypeError('View must be a string');
|
|
87
|
+
}
|
|
88
|
+
if (!view.length) {
|
|
89
|
+
throw new SyntaxError('View must not be empty');
|
|
90
|
+
}
|
|
91
|
+
if (!view.match(/^[a-z0-9/_-]+\.njk$/i)) {
|
|
92
|
+
throw new SyntaxError('View must contain only a-z, 0-9, -, _ and / characters, and end in .njk');
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
exports.validateView = validateView;
|
|
96
|
+
function validateHookName(hookName) {
|
|
97
|
+
if (typeof hookName !== 'string') {
|
|
98
|
+
throw new TypeError('Hook name must be a string');
|
|
99
|
+
}
|
|
100
|
+
if (!hookName.length) {
|
|
101
|
+
throw new SyntaxError('Hook name must not be empty');
|
|
102
|
+
}
|
|
103
|
+
if (!hookName.match(/^([a-z]+\.|)[a-z]+$/i)) {
|
|
104
|
+
throw new SyntaxError('Hook name must match either <scope>.<hookname> or <hookname> formats');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.validateHookName = validateHookName;
|
|
108
|
+
function validateHookPath(path) {
|
|
109
|
+
if (typeof path !== 'string' && !(path instanceof RegExp)) {
|
|
110
|
+
throw new TypeError('Hook path must be a string or RegExp');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
exports.validateHookPath = validateHookPath;
|
|
@@ -31,7 +31,7 @@ class InArray extends ValidatorFactory_js_1.default {
|
|
|
31
31
|
if (value !== null && typeof value !== 'undefined') {
|
|
32
32
|
const search = Array.isArray(value) ? value : [value];
|
|
33
33
|
for (let i = 0, l = search.length; i < l; i += 1) {
|
|
34
|
-
if (source.indexOf(search[i]) > -1) {
|
|
34
|
+
if (source.indexOf(search[parseInt(i, 10)]) > -1) {
|
|
35
35
|
valid = true;
|
|
36
36
|
}
|
|
37
37
|
else {
|
|
@@ -23,25 +23,3 @@ exports.default = {
|
|
|
23
23
|
strlen: strlen_js_1.default,
|
|
24
24
|
wordCount: wordCount_js_1.default,
|
|
25
25
|
};
|
|
26
|
-
// const dateObject = require('./dateObject.js');
|
|
27
|
-
// const email = require('./email.js');
|
|
28
|
-
// const inArray = require('./inArray.js');
|
|
29
|
-
// const nino = require('./nino.js');
|
|
30
|
-
// const optional = require('./optional.js');
|
|
31
|
-
// const postalAddressObject = require('./postalAddressObject.js');
|
|
32
|
-
// const regex = require('./regex.js');
|
|
33
|
-
// const required = require('./required.js');
|
|
34
|
-
// const strlen = require('./strlen.js');
|
|
35
|
-
// const wordCount = require('./wordCount.js');
|
|
36
|
-
// module.exports = {
|
|
37
|
-
// dateObject,
|
|
38
|
-
// email,
|
|
39
|
-
// inArray,
|
|
40
|
-
// nino,
|
|
41
|
-
// optional,
|
|
42
|
-
// postalAddressObject,
|
|
43
|
-
// regex,
|
|
44
|
-
// required,
|
|
45
|
-
// strlen,
|
|
46
|
-
// wordCount,
|
|
47
|
-
// };
|
|
@@ -66,6 +66,8 @@ class PostalAddressObject extends ValidatorFactory_js_1.default {
|
|
|
66
66
|
const reqF = Object.create(null);
|
|
67
67
|
const reqC = cfg.requiredFields;
|
|
68
68
|
['address1', 'address2', 'address3', 'address4', 'postcode'].forEach((k) => {
|
|
69
|
+
// ESLint disabled as `k` is a known value from a constant list
|
|
70
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
69
71
|
reqF[k] = reqC.indexOf(k) > -1;
|
|
70
72
|
});
|
|
71
73
|
let valid = true;
|
|
@@ -75,8 +77,7 @@ class PostalAddressObject extends ValidatorFactory_js_1.default {
|
|
|
75
77
|
const reAddrLine1 = /^\d+|[^\s]+[a-z0-9\-,.&#()/\\:;'" ]+$/i;
|
|
76
78
|
// UK Postcode regex taken from the dwp java pc checker
|
|
77
79
|
// https://github.com/dwp/postcode-format-validation
|
|
78
|
-
const
|
|
79
|
-
const rePostcode = new RegExp(pc, 'i');
|
|
80
|
+
const rePostcode = /^(?![QVX])[A-Z]((?![IJZ])[A-Z][0-9](([0-9]?)|([ABEHMNPRVWXY]?))|([0-9]([0-9]?|[ABCDEFGHJKPSTUW]?))) ?[0-9]((?![CIKMOV])[A-Z]){2}$|^(BFPO)[ ]?[0-9]{1,4}$/i;
|
|
80
81
|
// [required, regex, strlenmax, error message]
|
|
81
82
|
const attributes = {
|
|
82
83
|
address1: [reqF.address1, reAddrLine1, cfg.strlenmax, cfg.errorMsgAddress1],
|
|
@@ -85,6 +86,8 @@ class PostalAddressObject extends ValidatorFactory_js_1.default {
|
|
|
85
86
|
address4: [reqF.address4, reAddr, cfg.strlenmax, cfg.errorMsgAddress4],
|
|
86
87
|
postcode: [reqF.postcode, rePostcode, null, cfg.errorMsgPostcode],
|
|
87
88
|
};
|
|
89
|
+
// ESLint disabled as `k` is a known value from the constant list above
|
|
90
|
+
/* eslint-disable security/detect-object-injection */
|
|
88
91
|
Object.keys(attributes).forEach((k) => {
|
|
89
92
|
const attr = attributes[k];
|
|
90
93
|
const hasProperty = Object.prototype.hasOwnProperty.call(value, k);
|
|
@@ -100,6 +103,7 @@ class PostalAddressObject extends ValidatorFactory_js_1.default {
|
|
|
100
103
|
}));
|
|
101
104
|
}
|
|
102
105
|
});
|
|
106
|
+
/* eslint-enable security/detect-object-injection */
|
|
103
107
|
}
|
|
104
108
|
else {
|
|
105
109
|
valid = false;
|
|
@@ -14,9 +14,10 @@
|
|
|
14
14
|
export default function waypointUrl({ waypoint, mountUrl, journeyContext, edit, editOrigin, skipTo, routeName, }?: {
|
|
15
15
|
waypoint: string;
|
|
16
16
|
mountUrl: string;
|
|
17
|
-
journeyContext:
|
|
17
|
+
journeyContext: JourneyContext;
|
|
18
18
|
edit: boolean;
|
|
19
19
|
editOrigin: string;
|
|
20
20
|
skipTo: boolean;
|
|
21
21
|
routeName: string;
|
|
22
22
|
}): string;
|
|
23
|
+
export type JourneyContext = import('./index').JourneyContext;
|
package/dist/lib/waypoint-url.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @typedef {import('./index').JourneyContext} JourneyContext
|
|
4
|
+
*/
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
6
|
const reUrlProtocolExtract = /^url:\/\/(.+)$/i;
|
|
4
7
|
const sanitiseWaypoint = (w) => w.replace(/[^/a-z0-9_-]/ig, '').replace(/\/+/g, '/');
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyBody = void 0;
|
|
3
4
|
const express_1 = require("express");
|
|
5
|
+
const rProto = /__proto__/i;
|
|
6
|
+
const rPrototype = /prototype[='"[\]]/i;
|
|
7
|
+
const rConstructor = /constructor[='"[\]]/i;
|
|
8
|
+
function verifyBody(req, res, buf, encoding) {
|
|
9
|
+
const body = decodeURI(buf.toString(encoding));
|
|
10
|
+
if (rProto.test(body)) {
|
|
11
|
+
throw new Error('Request body verification failed (__proto__)');
|
|
12
|
+
}
|
|
13
|
+
if (rPrototype.test(body)) {
|
|
14
|
+
throw new Error('Request body verification failed (prototype)');
|
|
15
|
+
}
|
|
16
|
+
if (rConstructor.test(body)) {
|
|
17
|
+
throw new Error('Request body verification failed (constructor)');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
exports.verifyBody = verifyBody;
|
|
4
21
|
function bodyParserMiddleware() {
|
|
5
|
-
const rProto = /__proto__/i;
|
|
6
|
-
const rPrototype = /prototype[=[\]]/i;
|
|
7
|
-
const rConstructor = /constructor[=[\]]/i;
|
|
8
22
|
return [
|
|
9
23
|
(0, express_1.urlencoded)({
|
|
10
24
|
extended: true,
|
|
@@ -12,12 +26,7 @@ function bodyParserMiddleware() {
|
|
|
12
26
|
inflate: true,
|
|
13
27
|
parameterLimit: 25,
|
|
14
28
|
limit: 1024 * 50,
|
|
15
|
-
verify:
|
|
16
|
-
const body = decodeURI(buf.toString(encoding));
|
|
17
|
-
if (rProto.test(body) || rPrototype.test(body) || rConstructor.test(body)) {
|
|
18
|
-
throw new Error('Request body verification failed');
|
|
19
|
-
}
|
|
20
|
-
},
|
|
29
|
+
verify: verifyBody,
|
|
21
30
|
}),
|
|
22
31
|
];
|
|
23
32
|
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
export default function dataMiddleware({ plan, mountUrl,
|
|
1
|
+
export default function dataMiddleware({ plan, mountUrl, events, }: {
|
|
2
2
|
plan: any;
|
|
3
3
|
mountUrl: any;
|
|
4
|
-
serviceName: any;
|
|
5
4
|
events: any;
|
|
6
5
|
}): ((req: any, res: any, next: any) => void)[];
|
package/dist/middleware/data.js
CHANGED
|
@@ -18,7 +18,7 @@ const editOrigin = (req) => {
|
|
|
18
18
|
}
|
|
19
19
|
return '';
|
|
20
20
|
};
|
|
21
|
-
function dataMiddleware({ plan, mountUrl,
|
|
21
|
+
function dataMiddleware({ plan, mountUrl, events, }) {
|
|
22
22
|
return [
|
|
23
23
|
(req, res, next) => {
|
|
24
24
|
/* ------------------------------------------------ Request decorations */
|
|
@@ -37,19 +37,19 @@ function dataMiddleware({ plan, mountUrl, serviceName, events, }) {
|
|
|
37
37
|
// CASA and userland templates
|
|
38
38
|
res.locals.casa = {
|
|
39
39
|
mountUrl,
|
|
40
|
-
|
|
40
|
+
editMode: req.casa.editMode,
|
|
41
|
+
editOrigin: req.casa.editOrigin,
|
|
41
42
|
};
|
|
42
43
|
res.locals.locale = req.language;
|
|
43
44
|
// Used by govuk-frontend template
|
|
44
45
|
// - req.language is provided by i18n-http-middleware
|
|
45
46
|
res.locals.htmlLang = req.language;
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
//
|
|
51
|
-
|
|
52
|
-
// req.editSearchParams
|
|
47
|
+
// Function for building URLs. This will be curried with the `mountUrl`,
|
|
48
|
+
// `journeyContext`, `edit` and `editOrigin` for convenience. This means
|
|
49
|
+
// the template author does not have to be concerned about the current
|
|
50
|
+
// "state" when generating URLs, but still has the ability to override
|
|
51
|
+
// these curried defaults if needs be.
|
|
52
|
+
res.locals.waypointUrl = (args) => (0, waypoint_url_js_1.default)(Object.assign({ mountUrl, journeyContext: req.casa.journeyContext, edit: req.casa.editMode, editOrigin: req.casa.editOrigin }, args));
|
|
53
53
|
next();
|
|
54
54
|
},
|
|
55
55
|
];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports = __dirname;
|
|
1
|
+
module.exports = __dirname;
|
|
@@ -8,6 +8,9 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
8
8
|
};
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
10
|
const JourneyContext_js_1 = __importDefault(require("../lib/JourneyContext.js"));
|
|
11
|
+
/**
|
|
12
|
+
* @typedef {import('../lib/field').PageField} PageField
|
|
13
|
+
*/
|
|
11
14
|
/**
|
|
12
15
|
* Gather the field data from `req.body` into the current JourneyContext
|
|
13
16
|
* - Store in the current session
|
|
@@ -25,21 +28,19 @@ exports.default = ({ waypoint, fields = [], }) => [
|
|
|
25
28
|
// for any comparison work that may be done in subseqent middleware.
|
|
26
29
|
req.casa.archivedJourneyContext = JourneyContext_js_1.default.fromContext(req.casa.journeyContext);
|
|
27
30
|
// Ignore data for any non-persistent fields
|
|
31
|
+
// ESLint disabled as `fields`, `i` and `name` are dev-controlled
|
|
32
|
+
/* eslint-disable security/detect-object-injection */
|
|
28
33
|
const persistentBody = Object.create(null);
|
|
29
34
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
30
35
|
if (fields[i].meta.persist && fields[i].getValue(req.body) !== undefined) {
|
|
31
36
|
persistentBody[fields[i].name] = fields[i].getValue(req.body);
|
|
32
37
|
}
|
|
33
38
|
}
|
|
39
|
+
/* eslint-enable security/detect-object-injection */
|
|
34
40
|
// Update data and validation context in the current request, and store
|
|
35
41
|
req.casa.journeyContext.setDataForPage(waypoint, persistentBody);
|
|
36
42
|
req.casa.journeyContext.removeValidationStateForPage(waypoint);
|
|
37
43
|
JourneyContext_js_1.default.putContext(req.session, req.casa.journeyContext);
|
|
38
|
-
// TODO: Once feature we might like here is to invalidate specific pages
|
|
39
|
-
// based on the change that has just happened, to force those pages to be
|
|
40
|
-
// visited again. The main use case is to cater for a change in content on
|
|
41
|
-
// those pages that fundamentally alter the context of the question being asked,
|
|
42
|
-
// and so should be asked again. For example, "Your address" vs "You and your partner's address"
|
|
43
44
|
next();
|
|
44
45
|
},
|
|
45
46
|
];
|
package/dist/middleware/i18n.js
CHANGED
|
@@ -10,7 +10,7 @@ const fs_1 = require("fs");
|
|
|
10
10
|
const deepmerge_1 = __importDefault(require("deepmerge"));
|
|
11
11
|
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
12
12
|
const logger_js_1 = __importDefault(require("../lib/logger.js"));
|
|
13
|
-
const log = (0, logger_js_1.default)('middleware
|
|
13
|
+
const log = (0, logger_js_1.default)('middleware:i18n');
|
|
14
14
|
const loadJson = (file) => {
|
|
15
15
|
// Strip out newlines (this is a legacy feature which we're keeping for
|
|
16
16
|
// backwards compatibility).
|
|
@@ -29,6 +29,9 @@ const extract = (file) => {
|
|
|
29
29
|
const loadResources = (languages, directories) => {
|
|
30
30
|
const store = Object.create(null);
|
|
31
31
|
languages.forEach((language) => {
|
|
32
|
+
// ESLint disabled as `store`, `language` and `ns` are all dev-controlled,
|
|
33
|
+
// and this function is only called once, at boot-time.
|
|
34
|
+
/* eslint-disable security/detect-object-injection */
|
|
32
35
|
store[language] = Object.create(null);
|
|
33
36
|
directories.forEach((basedir) => {
|
|
34
37
|
const dir = (0, path_1.resolve)(basedir, language);
|
|
@@ -44,6 +47,7 @@ const loadResources = (languages, directories) => {
|
|
|
44
47
|
store[language][ns] = (0, deepmerge_1.default)(store[language][ns], data);
|
|
45
48
|
});
|
|
46
49
|
});
|
|
50
|
+
/* eslint-enable security/detect-object-injection */
|
|
47
51
|
});
|
|
48
52
|
return store;
|
|
49
53
|
};
|
package/dist/middleware/post.js
CHANGED
|
@@ -26,31 +26,31 @@ function postMiddleware({ mountUrl, }) {
|
|
|
26
26
|
// CSRF token is invalid in some way
|
|
27
27
|
if ((err === null || err === void 0 ? void 0 : err.code) === 'EBADCSRFTOKEN') {
|
|
28
28
|
log.info('CSRF validation has failed. This may be caused by the user submitting a stale form from a previous session [EBADCSRFTOKEN]');
|
|
29
|
-
return res.status(403).render(TEMPLATE);
|
|
29
|
+
return res.status(403).render(TEMPLATE, { errorCode: 'bad_csrf_token' });
|
|
30
30
|
}
|
|
31
31
|
// Body parsing verification check failed
|
|
32
32
|
if ((err === null || err === void 0 ? void 0 : err.type) === 'entity.verify.failed') {
|
|
33
33
|
log.info('Body parser verification has failed. This has been caused by the user submitting a payload containing invalid data [entity.verify.failed]');
|
|
34
|
-
return res.status(403).render(TEMPLATE);
|
|
34
|
+
return res.status(403).render(TEMPLATE, { errorCode: 'invalid_payload' });
|
|
35
35
|
}
|
|
36
36
|
// Too many parameters submitted
|
|
37
37
|
if ((err === null || err === void 0 ? void 0 : err.type) === 'parameters.too.many') {
|
|
38
38
|
log.info('The request contains more parameters than is currently allowed [parameters.too.many]');
|
|
39
|
-
return res.status(413).render(TEMPLATE);
|
|
39
|
+
return res.status(413).render(TEMPLATE, { errorCode: 'parameter_limit_exceeded' });
|
|
40
40
|
}
|
|
41
41
|
// Overall payload too large
|
|
42
42
|
if ((err === null || err === void 0 ? void 0 : err.type) === 'entity.too.large') {
|
|
43
43
|
log.info(`The request payload is too large. Received ${err.length}b with a maximum of ${err.limit}b [parameters.too.many]`);
|
|
44
|
-
return res.status(413).render(TEMPLATE);
|
|
44
|
+
return res.status(413).render(TEMPLATE, { errorCode: 'payload_size_exceeded' });
|
|
45
45
|
}
|
|
46
46
|
// Unaccept request method
|
|
47
47
|
if ((err === null || err === void 0 ? void 0 : err.code) === 'unaccepted_request_method') {
|
|
48
48
|
log.info(err.message);
|
|
49
|
-
return res.status(400).render(TEMPLATE);
|
|
49
|
+
return res.status(400).render(TEMPLATE, { errorCode: 'unaccepted_request_method' });
|
|
50
50
|
}
|
|
51
51
|
// Unknown error
|
|
52
52
|
log.error(`Unknown error: ${err.message}; stacktrace: ${err.stack}`);
|
|
53
|
-
return res.render(TEMPLATE);
|
|
53
|
+
return res.status(200).render(TEMPLATE);
|
|
54
54
|
},
|
|
55
55
|
];
|
|
56
56
|
}
|
|
@@ -26,7 +26,7 @@ exports.default = ({ waypoint, plan, mountUrl, }) => [
|
|
|
26
26
|
const traversed = plan.traverse(req.casa.journeyContext);
|
|
27
27
|
const currentIndex = traversed.indexOf(waypoint);
|
|
28
28
|
const nextIndex = Math.max(currentIndex < 0 ? traversed.length - 1 : 0, Math.min(currentIndex + 1, traversed.length - 1));
|
|
29
|
-
const nextWaypoint = traversed[nextIndex];
|
|
29
|
+
const nextWaypoint = traversed[parseInt(nextIndex, 10)];
|
|
30
30
|
log.trace(`currentIndex = ${currentIndex}, nextIndex = ${nextIndex}, currentWaypoint = ${waypoint}, nextWaypoint = ${nextWaypoint}`);
|
|
31
31
|
// Edit mode
|
|
32
32
|
// Attempt to take the user back to their original URL. We rely on the
|
|
@@ -11,32 +11,32 @@ const field_js_1 = __importDefault(require("../lib/field.js"));
|
|
|
11
11
|
const JourneyContext_js_1 = __importDefault(require("../lib/JourneyContext.js"));
|
|
12
12
|
exports.default = ({ waypoint, fields = [], }) => {
|
|
13
13
|
// Add some common, transient fields to ensure they survive beyond this sanitisation process
|
|
14
|
-
fields.push((0, field_js_1.default)('_csrf', { persist: false }).processor((
|
|
15
|
-
fields.push((0, field_js_1.default)('contextid', { persist: false }).processor((
|
|
16
|
-
fields.push((0, field_js_1.default)('edit', { persist: false }).processor((
|
|
17
|
-
fields.push((0, field_js_1.default)('editorigin', { persist: false }).processor((
|
|
14
|
+
fields.push((0, field_js_1.default)('_csrf', { persist: false }).processor((value) => String(value)));
|
|
15
|
+
fields.push((0, field_js_1.default)('contextid', { persist: false }).processor((value) => String(value)));
|
|
16
|
+
fields.push((0, field_js_1.default)('edit', { persist: false }).processor((value) => String(value)));
|
|
17
|
+
fields.push((0, field_js_1.default)('editorigin', { persist: false }).processor((value) => String(value)));
|
|
18
18
|
// Middleware
|
|
19
19
|
return [
|
|
20
20
|
(req, res, next) => {
|
|
21
21
|
// First, prune all undefined, or unknown fields from `req.body` (i.e.
|
|
22
22
|
// those that do not have an entry in `fields`)
|
|
23
|
+
// EsLint disabled as `fields`, `i` & `name` are only controlled by dev
|
|
24
|
+
/* eslint-disable security/detect-object-injection */
|
|
23
25
|
const prunedBody = Object.create(null);
|
|
24
26
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
25
27
|
if (lodash_1.default.has(req.body, fields[i].name) && req.body[fields[i].name] !== undefined) {
|
|
26
28
|
prunedBody[fields[i].name] = req.body[fields[i].name];
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
|
-
|
|
30
|
-
// data set to the "prunedBody"
|
|
31
|
+
/* eslint-enable security/detect-object-injection */
|
|
31
32
|
const journeyContext = JourneyContext_js_1.default.fromContext(req.casa.journeyContext);
|
|
32
33
|
journeyContext.setDataForPage(waypoint, prunedBody);
|
|
33
|
-
// const journeyContext = {};
|
|
34
34
|
// Second, prune any fields that do not pass the validation conditional,
|
|
35
35
|
// and process those that do.
|
|
36
36
|
const sanitisedBody = Object.create(null);
|
|
37
37
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
38
|
-
const field = fields[i];
|
|
39
|
-
const fieldValue = field.getValue(prunedBody);
|
|
38
|
+
const field = fields[i]; /* eslint-disable-line security/detect-object-injection */
|
|
39
|
+
const fieldValue = field.getValue(prunedBody);
|
|
40
40
|
if (fieldValue !== undefined && field.testConditions({
|
|
41
41
|
fieldValue,
|
|
42
42
|
waypoint,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export default function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl, cookieSameSite, store, }: {
|
|
1
|
+
export default function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl, cookieSameSite, cookiePath, store, }: {
|
|
2
2
|
cookieParserMiddleware: any;
|
|
3
3
|
secret: any;
|
|
4
4
|
name: any;
|
|
@@ -6,5 +6,6 @@ export default function sessionMiddleware({ cookieParserMiddleware, secret, name
|
|
|
6
6
|
ttl: any;
|
|
7
7
|
mountUrl?: string | undefined;
|
|
8
8
|
cookieSameSite?: boolean | undefined;
|
|
9
|
+
cookiePath?: string | undefined;
|
|
9
10
|
store?: any;
|
|
10
11
|
}): any[];
|
|
@@ -28,14 +28,65 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
28
28
|
const express_session_1 = __importStar(require("express-session"));
|
|
29
29
|
const logger_js_1 = __importDefault(require("../lib/logger.js"));
|
|
30
30
|
const log = (0, logger_js_1.default)('middleware:session');
|
|
31
|
+
const sessionExpiryMiddleware = (mountUrl, ttl, getCookie, touchCookie, removeCookie) => (req, res, next) => {
|
|
32
|
+
var _a;
|
|
33
|
+
const lastModified = getCookie(req);
|
|
34
|
+
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
35
|
+
if (lastModified === 0) {
|
|
36
|
+
// New session, or grace period cookie no longer available after
|
|
37
|
+
// expiring; generate a new session, and create grace-period cookie.
|
|
38
|
+
// This will invalidate any CSRF tokens, so by letting the request POST
|
|
39
|
+
// requests through the user may see a 500 error response.
|
|
40
|
+
log.info('Session is new, or grace period has expired. Regenerating session.');
|
|
41
|
+
req.session.regenerate((err) => {
|
|
42
|
+
if (err) {
|
|
43
|
+
next(err);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
touchCookie(res);
|
|
47
|
+
if (req.method === 'POST') {
|
|
48
|
+
log.info('The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.');
|
|
49
|
+
res.redirect(302, mountUrl);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
next();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else if (age > ttl) {
|
|
58
|
+
// Cookie has become stale and server session will have been removed;
|
|
59
|
+
// redirect to session-timeout
|
|
60
|
+
log.info('Session has timed out within grace period. Destroying session and redirecting to timeout page.');
|
|
61
|
+
const language = (_a = req.session.language) !== null && _a !== void 0 ? _a : 'en';
|
|
62
|
+
req.session.destroy((err) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
next(err);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
removeCookie(res);
|
|
68
|
+
const params = new URLSearchParams({
|
|
69
|
+
referrer: req.originalUrl,
|
|
70
|
+
lang: language,
|
|
71
|
+
});
|
|
72
|
+
res.redirect(302, `${mountUrl}session-timeout?${params.toString()}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Touch cookie and continue
|
|
78
|
+
touchCookie(res);
|
|
79
|
+
next();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
31
82
|
// 3 middleware:
|
|
32
83
|
// - set the session cookie
|
|
33
84
|
// - parse request cookies
|
|
34
85
|
// - handle expiry of server-side session
|
|
35
|
-
function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl = '/', cookieSameSite = true, store = new express_session_1.MemoryStore(), }) {
|
|
86
|
+
function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl = '/', cookieSameSite = true, cookiePath = '/', store = new express_session_1.MemoryStore(), }) {
|
|
36
87
|
const commonCookieOptions = {
|
|
37
88
|
httpOnly: true,
|
|
38
|
-
path:
|
|
89
|
+
path: cookiePath,
|
|
39
90
|
secure,
|
|
40
91
|
};
|
|
41
92
|
if (cookieSameSite !== false) {
|
|
@@ -44,8 +95,15 @@ function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl,
|
|
|
44
95
|
const ttlGrace = 1800; // user will see session-timeout if session expires within 30mins
|
|
45
96
|
const touchCookieName = `${name}.t`;
|
|
46
97
|
const touchCookieOptions = Object.assign(Object.assign({}, commonCookieOptions), { maxAge: (ttl + ttlGrace) * 1000, signed: true });
|
|
98
|
+
const getCookie = (req) => {
|
|
99
|
+
var _a;
|
|
100
|
+
// Disabled eslint as `touchCookieName` is a constant, known value
|
|
101
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
102
|
+
const lastModified = Date.parse(String((_a = req.signedCookies[touchCookieName]) !== null && _a !== void 0 ? _a : '1970-01-01T00:00:00+0000'));
|
|
103
|
+
return Number.isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001);
|
|
104
|
+
};
|
|
47
105
|
const touchCookie = (res) => {
|
|
48
|
-
// Touch cookie expiry is
|
|
106
|
+
// Touch cookie expiry is a short period after the session ttl. This gives
|
|
49
107
|
// a small period of time where a user will see the session-timeout message,
|
|
50
108
|
// which is important to avoid the confusion of simply being redirected back
|
|
51
109
|
// to the start of their journey.
|
|
@@ -64,58 +122,7 @@ function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl,
|
|
|
64
122
|
store,
|
|
65
123
|
}),
|
|
66
124
|
cookieParserMiddleware,
|
|
67
|
-
(
|
|
68
|
-
var _a, _b;
|
|
69
|
-
let lastModified = Date.parse(String((_a = req.signedCookies[touchCookieName]) !== null && _a !== void 0 ? _a : '1970-01-01T00:00:00+0000'));
|
|
70
|
-
lastModified = Number.isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001);
|
|
71
|
-
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
72
|
-
if (lastModified === 0) {
|
|
73
|
-
// New session, or grace period cookie no longer available after
|
|
74
|
-
// expiring; generate a new session, and create grace-period cookie.
|
|
75
|
-
// This will invalidate any CSRF tokens, so by letting the request POST
|
|
76
|
-
// requests through the user may see a 500 error response.
|
|
77
|
-
log.info('Session is new, or grace period has expired. Regenerating session.');
|
|
78
|
-
req.session.regenerate((err) => {
|
|
79
|
-
if (err) {
|
|
80
|
-
next(err);
|
|
81
|
-
}
|
|
82
|
-
else {
|
|
83
|
-
touchCookie(res);
|
|
84
|
-
if (req.method === 'POST') {
|
|
85
|
-
log.info('The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.');
|
|
86
|
-
res.redirect(302, mountUrl);
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
next();
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
else if (age > ttl) {
|
|
95
|
-
// Cookie has become stale and server session will have been removed;
|
|
96
|
-
// redirect to session-timeout
|
|
97
|
-
log.info('Session has timed out within grace period. Destroying session and redirecting to timeout page.');
|
|
98
|
-
const language = (_b = req.session.language) !== null && _b !== void 0 ? _b : 'en';
|
|
99
|
-
req.session.destroy((err) => {
|
|
100
|
-
if (err) {
|
|
101
|
-
next(err);
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
removeCookie(res);
|
|
105
|
-
const params = new URLSearchParams({
|
|
106
|
-
referrer: req.originalUrl,
|
|
107
|
-
lang: language,
|
|
108
|
-
});
|
|
109
|
-
res.redirect(302, `${mountUrl}session-timeout?${params.toString()}`);
|
|
110
|
-
}
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
else {
|
|
114
|
-
// Touch cookie and continue
|
|
115
|
-
touchCookie(res);
|
|
116
|
-
next();
|
|
117
|
-
}
|
|
118
|
-
},
|
|
125
|
+
sessionExpiryMiddleware(mountUrl, ttl, getCookie, touchCookie, removeCookie),
|
|
119
126
|
];
|
|
120
127
|
}
|
|
121
128
|
exports.default = sessionMiddleware;
|
|
@@ -26,8 +26,8 @@ exports.default = ({ waypoint, mountUrl, }) => [
|
|
|
26
26
|
const redirectUrl = (0, waypoint_url_js_1.default)({
|
|
27
27
|
mountUrl,
|
|
28
28
|
waypoint: skipTo,
|
|
29
|
-
edit: req.editMode,
|
|
30
|
-
editOrigin: req.
|
|
29
|
+
edit: req.casa.editMode,
|
|
30
|
+
editOrigin: req.casa.editOrigin,
|
|
31
31
|
journeyContext: req.casa.journeyContext,
|
|
32
32
|
});
|
|
33
33
|
log.debug(`Will redirect to "${redirectUrl}" after skipping "${waypoint}"`);
|