@dwp/govuk-casa 9.0.0 → 9.1.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 +9 -9
- package/dist/assets/css/casa.css +1 -1
- package/dist/assets/css/casa.css.map +1 -1
- package/dist/casa.d.ts +122 -95
- package/dist/casa.js +119 -86
- package/dist/casa.js.map +1 -1
- package/dist/lib/CasaTemplateLoader.d.ts +4 -4
- package/dist/lib/CasaTemplateLoader.js +16 -16
- package/dist/lib/CasaTemplateLoader.js.map +1 -1
- package/dist/lib/JourneyContext.d.ts +38 -40
- package/dist/lib/JourneyContext.js +81 -75
- package/dist/lib/JourneyContext.js.map +1 -1
- package/dist/lib/MutableRouter.d.ts +40 -41
- package/dist/lib/MutableRouter.js +64 -71
- package/dist/lib/MutableRouter.js.map +1 -1
- package/dist/lib/Plan.d.ts +29 -26
- package/dist/lib/Plan.js +85 -71
- package/dist/lib/Plan.js.map +1 -1
- package/dist/lib/ValidationError.d.ts +16 -15
- package/dist/lib/ValidationError.js +21 -20
- package/dist/lib/ValidationError.js.map +1 -1
- package/dist/lib/ValidatorFactory.d.ts +15 -13
- package/dist/lib/ValidatorFactory.js +14 -12
- package/dist/lib/ValidatorFactory.js.map +1 -1
- package/dist/lib/configuration-ingestor.d.ts +37 -40
- package/dist/lib/configuration-ingestor.js +93 -93
- package/dist/lib/configuration-ingestor.js.map +1 -1
- package/dist/lib/configure.d.ts +6 -6
- package/dist/lib/configure.js +14 -12
- package/dist/lib/configure.js.map +1 -1
- package/dist/lib/constants.d.ts +1 -3
- package/dist/lib/constants.js +9 -11
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/context-id-generators.d.ts +3 -5
- package/dist/lib/context-id-generators.js +7 -6
- package/dist/lib/context-id-generators.js.map +1 -1
- package/dist/lib/end-session.d.ts +4 -4
- package/dist/lib/end-session.js +5 -5
- package/dist/lib/field.d.ts +20 -18
- package/dist/lib/field.js +35 -48
- package/dist/lib/field.js.map +1 -1
- package/dist/lib/index.d.ts +13 -13
- package/dist/lib/logger.d.ts +7 -6
- package/dist/lib/logger.js +7 -7
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/mount.d.ts +5 -5
- package/dist/lib/mount.js +11 -10
- package/dist/lib/mount.js.map +1 -1
- package/dist/lib/nunjucks-filters.d.ts +10 -12
- package/dist/lib/nunjucks-filters.js +35 -35
- package/dist/lib/nunjucks-filters.js.map +1 -1
- package/dist/lib/nunjucks.d.ts +7 -5
- package/dist/lib/nunjucks.js +10 -8
- package/dist/lib/nunjucks.js.map +1 -1
- package/dist/lib/utils.d.ts +19 -19
- package/dist/lib/utils.js +62 -55
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/validators/dateObject.d.ts +29 -22
- package/dist/lib/validators/dateObject.js +58 -49
- package/dist/lib/validators/dateObject.js.map +1 -1
- package/dist/lib/validators/email.d.ts +4 -4
- package/dist/lib/validators/email.js +4 -4
- package/dist/lib/validators/inArray.d.ts +4 -4
- package/dist/lib/validators/inArray.js +7 -8
- package/dist/lib/validators/inArray.js.map +1 -1
- package/dist/lib/validators/index.d.ts +10 -10
- package/dist/lib/validators/index.js +1 -3
- package/dist/lib/validators/index.js.map +1 -1
- package/dist/lib/validators/nino.d.ts +9 -8
- package/dist/lib/validators/nino.js +14 -10
- package/dist/lib/validators/nino.js.map +1 -1
- package/dist/lib/validators/postalAddressObject.d.ts +37 -24
- package/dist/lib/validators/postalAddressObject.js +65 -46
- package/dist/lib/validators/postalAddressObject.js.map +1 -1
- package/dist/lib/validators/range.d.ts +12 -8
- package/dist/lib/validators/range.js +11 -9
- package/dist/lib/validators/range.js.map +1 -1
- package/dist/lib/validators/regex.d.ts +4 -4
- package/dist/lib/validators/regex.js +5 -5
- package/dist/lib/validators/required.d.ts +6 -6
- package/dist/lib/validators/required.js +9 -11
- package/dist/lib/validators/required.js.map +1 -1
- package/dist/lib/validators/strlen.d.ts +12 -8
- package/dist/lib/validators/strlen.js +13 -11
- package/dist/lib/validators/strlen.js.map +1 -1
- package/dist/lib/validators/wordCount.d.ts +12 -8
- package/dist/lib/validators/wordCount.js +15 -11
- package/dist/lib/validators/wordCount.js.map +1 -1
- package/dist/lib/waypoint-url.d.ts +16 -13
- package/dist/lib/waypoint-url.js +39 -36
- package/dist/lib/waypoint-url.js.map +1 -1
- package/dist/middleware/body-parser.d.ts +1 -1
- package/dist/middleware/body-parser.js +6 -6
- package/dist/middleware/body-parser.js.map +1 -1
- package/dist/middleware/data.d.ts +1 -1
- package/dist/middleware/data.js +8 -7
- package/dist/middleware/data.js.map +1 -1
- package/dist/middleware/gather-fields.d.ts +2 -2
- package/dist/middleware/gather-fields.js +6 -4
- package/dist/middleware/gather-fields.js.map +1 -1
- package/dist/middleware/i18n.js +13 -15
- package/dist/middleware/i18n.js.map +1 -1
- package/dist/middleware/post.js +30 -18
- package/dist/middleware/post.js.map +1 -1
- package/dist/middleware/pre.d.ts +2 -2
- package/dist/middleware/pre.js +46 -27
- package/dist/middleware/pre.js.map +1 -1
- package/dist/middleware/progress-journey.d.ts +1 -1
- package/dist/middleware/progress-journey.js +5 -5
- package/dist/middleware/progress-journey.js.map +1 -1
- package/dist/middleware/sanitise-fields.d.ts +1 -1
- package/dist/middleware/sanitise-fields.js +13 -11
- package/dist/middleware/sanitise-fields.js.map +1 -1
- package/dist/middleware/serve-first-waypoint.d.ts +3 -3
- package/dist/middleware/serve-first-waypoint.js +8 -6
- package/dist/middleware/serve-first-waypoint.js.map +1 -1
- package/dist/middleware/session.js +14 -11
- package/dist/middleware/session.js.map +1 -1
- package/dist/middleware/skip-waypoint.d.ts +1 -1
- package/dist/middleware/skip-waypoint.js +3 -3
- package/dist/middleware/skip-waypoint.js.map +1 -1
- package/dist/middleware/steer-journey.d.ts +1 -1
- package/dist/middleware/steer-journey.js +16 -14
- package/dist/middleware/steer-journey.js.map +1 -1
- package/dist/middleware/strip-proxy-path.d.ts +1 -1
- package/dist/middleware/strip-proxy-path.js +3 -3
- package/dist/middleware/strip-proxy-path.js.map +1 -1
- package/dist/middleware/validate-fields.d.ts +1 -1
- package/dist/middleware/validate-fields.js +2 -5
- package/dist/middleware/validate-fields.js.map +1 -1
- package/dist/routes/ancillary.d.ts +3 -3
- package/dist/routes/ancillary.js +4 -4
- package/dist/routes/ancillary.js.map +1 -1
- package/dist/routes/journey.d.ts +2 -2
- package/dist/routes/journey.js +91 -39
- package/dist/routes/journey.js.map +1 -1
- package/dist/routes/static.d.ts +7 -5
- package/dist/routes/static.js +20 -19
- package/dist/routes/static.js.map +1 -1
- package/package.json +19 -18
- package/src/casa.js +133 -100
- package/src/lib/CasaTemplateLoader.js +24 -19
- package/src/lib/JourneyContext.js +138 -107
- package/src/lib/MutableRouter.js +72 -74
- package/src/lib/Plan.js +145 -97
- package/src/lib/ValidationError.js +25 -21
- package/src/lib/ValidatorFactory.js +17 -13
- package/src/lib/configuration-ingestor.js +147 -110
- package/src/lib/configure.js +34 -32
- package/src/lib/constants.js +9 -11
- package/src/lib/context-id-generators.js +40 -43
- package/src/lib/end-session.js +6 -6
- package/src/lib/field.js +69 -58
- package/src/lib/index.js +12 -12
- package/src/lib/logger.js +9 -9
- package/src/lib/mount.js +70 -74
- package/src/lib/nunjucks-filters.js +56 -59
- package/src/lib/nunjucks.js +23 -18
- package/src/lib/utils.js +78 -57
- package/src/lib/validators/dateObject.js +71 -60
- package/src/lib/validators/email.js +8 -8
- package/src/lib/validators/inArray.js +10 -11
- package/src/lib/validators/index.js +12 -14
- package/src/lib/validators/nino.js +29 -15
- package/src/lib/validators/postalAddressObject.js +87 -63
- package/src/lib/validators/range.js +14 -12
- package/src/lib/validators/regex.js +8 -8
- package/src/lib/validators/required.js +16 -16
- package/src/lib/validators/strlen.js +16 -14
- package/src/lib/validators/wordCount.js +22 -14
- package/src/lib/waypoint-url.js +64 -46
- package/src/middleware/body-parser.js +10 -10
- package/src/middleware/csrf.js +1 -1
- package/src/middleware/data.js +28 -24
- package/src/middleware/gather-fields.js +10 -9
- package/src/middleware/i18n.js +35 -37
- package/src/middleware/post.js +41 -21
- package/src/middleware/pre.js +62 -41
- package/src/middleware/progress-journey.js +32 -18
- package/src/middleware/sanitise-fields.js +43 -20
- package/src/middleware/serve-first-waypoint.js +14 -12
- package/src/middleware/session.js +74 -61
- package/src/middleware/skip-waypoint.js +7 -9
- package/src/middleware/steer-journey.js +40 -28
- package/src/middleware/strip-proxy-path.js +8 -7
- package/src/middleware/validate-fields.js +5 -12
- package/src/routes/ancillary.js +5 -7
- package/src/routes/journey.js +159 -85
- package/src/routes/static.js +62 -29
- package/views/casa/components/character-count/README.md +2 -2
- package/views/casa/components/checkboxes/README.md +6 -6
- package/views/casa/components/date-input/README.md +7 -7
- package/views/casa/components/input/README.md +2 -2
- package/views/casa/components/journey-form/README.md +33 -14
- package/views/casa/components/postal-address-object/README.md +4 -4
- package/views/casa/components/radios/README.md +6 -6
- package/views/casa/components/select/README.md +6 -6
- package/views/casa/components/textarea/README.md +2 -2
- package/views/casa/layouts/main.njk +2 -1
package/src/middleware/pre.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { randomBytes } from
|
|
2
|
-
import helmet from
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
import helmet from "helmet";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
+
* @typedef {import("../casa").HelmetConfigurator} HelmetConfigurator
|
|
5
6
|
* @access private
|
|
6
|
-
* @typedef {import('../casa').HelmetConfigurator} HelmetConfigurator
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
const GA_DOMAIN =
|
|
10
|
-
const GA_ANALYTICS_DOMAIN =
|
|
11
|
-
const GTM_DOMAIN =
|
|
12
|
-
const GTM_PREVIEW_DOMAIN =
|
|
9
|
+
const GA_DOMAIN = "*.google-analytics.com";
|
|
10
|
+
const GA_ANALYTICS_DOMAIN = "*.analytics.google.com";
|
|
11
|
+
const GTM_DOMAIN = "*.googletagmanager.com";
|
|
12
|
+
const GTM_PREVIEW_DOMAIN = "https://tagmanager.google.com";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Extracts the CSP nonce used in every template, and makes it available as a
|
|
@@ -19,9 +19,9 @@ const GTM_PREVIEW_DOMAIN = 'https://tagmanager.google.com';
|
|
|
19
19
|
* to identify this function specifically, most likely to remove it from CSP
|
|
20
20
|
* headers for custom purposes.
|
|
21
21
|
*
|
|
22
|
-
* @param {import(
|
|
23
|
-
* @param {import(
|
|
24
|
-
* @returns {string}
|
|
22
|
+
* @param {import("express").Request} req Request
|
|
23
|
+
* @param {import("express").Response} res Response
|
|
24
|
+
* @returns {string} Nonce value suitable for use in CSP header
|
|
25
25
|
*/
|
|
26
26
|
function casaCspNonce(req, res) {
|
|
27
27
|
return `'nonce-${res.locals.cspNonce}'`;
|
|
@@ -31,17 +31,18 @@ function casaCspNonce(req, res) {
|
|
|
31
31
|
* Pre middleware.
|
|
32
32
|
*
|
|
33
33
|
* @param {object} opts Options
|
|
34
|
-
* @param {HelmetConfigurator} opts.helmetConfigurator Function to customise
|
|
34
|
+
* @param {HelmetConfigurator} opts.helmetConfigurator Function to customise
|
|
35
|
+
* Helmet configuration
|
|
35
36
|
* @returns {Function[]} List of middleware
|
|
36
37
|
*/
|
|
37
|
-
export default ({
|
|
38
|
-
helmetConfigurator = (config) => (config),
|
|
39
|
-
} = {}) => [
|
|
38
|
+
export default ({ helmetConfigurator = (config) => config } = {}) => [
|
|
40
39
|
// Only allow certain request methods
|
|
41
40
|
(req, res, next) => {
|
|
42
|
-
if (req.method !==
|
|
43
|
-
const err = new Error(
|
|
44
|
-
|
|
41
|
+
if (req.method !== "GET" && req.method !== "POST") {
|
|
42
|
+
const err = new Error(
|
|
43
|
+
`Unaccepted request method, "${String(req.method).substr(0, 7)}"`,
|
|
44
|
+
);
|
|
45
|
+
err.code = "unaccepted_request_method";
|
|
45
46
|
next(err);
|
|
46
47
|
} else {
|
|
47
48
|
next();
|
|
@@ -53,40 +54,60 @@ export default ({
|
|
|
53
54
|
// The `no-store` setting is to specifically disable the bfcache and prevent
|
|
54
55
|
// possible leakage of information.
|
|
55
56
|
(req, res, next) => {
|
|
56
|
-
res.set(
|
|
57
|
-
res.set(
|
|
58
|
-
res.set(
|
|
59
|
-
res.set(
|
|
57
|
+
res.set("cache-control", "no-cache, no-store, must-revalidate, private");
|
|
58
|
+
res.set("pragma", "no-cache");
|
|
59
|
+
res.set("expires", 0);
|
|
60
|
+
res.set("x-robots-tag", "noindex, nofollow");
|
|
60
61
|
next();
|
|
61
62
|
},
|
|
62
63
|
|
|
63
64
|
// Generate nonces ready for use in Content-Security-Policy header and
|
|
64
65
|
// govuk-frontend template. This same none can be used wherever required.
|
|
65
66
|
(req, res, next) => {
|
|
66
|
-
res.locals.cspNonce = randomBytes(16).toString(
|
|
67
|
+
res.locals.cspNonce = randomBytes(16).toString("hex");
|
|
67
68
|
next();
|
|
68
69
|
},
|
|
69
70
|
|
|
70
71
|
// Helmet suite of headers
|
|
71
|
-
helmet(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
72
|
+
helmet(
|
|
73
|
+
helmetConfigurator({
|
|
74
|
+
// Allows GA which is typically used, and a known inline script nonce
|
|
75
|
+
contentSecurityPolicy: {
|
|
76
|
+
useDefaults: true,
|
|
77
|
+
directives: {
|
|
78
|
+
"default-src": ["'none'"],
|
|
79
|
+
"script-src": [
|
|
80
|
+
"'self'",
|
|
81
|
+
GA_DOMAIN,
|
|
82
|
+
GTM_DOMAIN,
|
|
83
|
+
GTM_PREVIEW_DOMAIN,
|
|
84
|
+
casaCspNonce,
|
|
85
|
+
],
|
|
86
|
+
"img-src": [
|
|
87
|
+
"'self'",
|
|
88
|
+
GA_DOMAIN,
|
|
89
|
+
GA_ANALYTICS_DOMAIN,
|
|
90
|
+
GTM_DOMAIN,
|
|
91
|
+
"https://ssl.gstatic.com",
|
|
92
|
+
"https://www.gstatic.com",
|
|
93
|
+
],
|
|
94
|
+
"connect-src": ["'self'", GA_DOMAIN, GA_ANALYTICS_DOMAIN, GTM_DOMAIN],
|
|
95
|
+
"frame-src": ["'self'", GTM_DOMAIN],
|
|
96
|
+
"frame-ancestors": ["'self'"],
|
|
97
|
+
"form-action": ["'self'"],
|
|
98
|
+
"style-src": [
|
|
99
|
+
"'self'",
|
|
100
|
+
"https://fonts.googleapis.com",
|
|
101
|
+
GTM_PREVIEW_DOMAIN,
|
|
102
|
+
casaCspNonce,
|
|
103
|
+
],
|
|
104
|
+
"font-src": ["'self'", "data:", "https://fonts.gstatic.com"],
|
|
105
|
+
"manifest-src": ["'self'"],
|
|
106
|
+
},
|
|
86
107
|
},
|
|
87
|
-
},
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
109
|
+
// // Require referrer to aid navigation
|
|
110
|
+
// referrerPolicy: 'no-referrer, same-origin',
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
92
113
|
];
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
// We assume that the waypoint has been validated prior to reaching this
|
|
3
3
|
// middleware.
|
|
4
4
|
|
|
5
|
-
import Plan from
|
|
6
|
-
import JourneyContext from
|
|
7
|
-
import waypointUrl from
|
|
8
|
-
import logger from
|
|
9
|
-
import { REQUEST_PHASE_REDIRECT } from
|
|
5
|
+
import Plan from "../lib/Plan.js";
|
|
6
|
+
import JourneyContext from "../lib/JourneyContext.js";
|
|
7
|
+
import waypointUrl from "../lib/waypoint-url.js";
|
|
8
|
+
import logger from "../lib/logger.js";
|
|
9
|
+
import { REQUEST_PHASE_REDIRECT } from "../lib/constants.js";
|
|
10
10
|
|
|
11
|
-
const log = logger(
|
|
11
|
+
const log = logger("middleware:progress-journey");
|
|
12
12
|
|
|
13
13
|
const saveAndRedirect = (session, journeyContext, url, res, next) => {
|
|
14
14
|
JourneyContext.putContext(session, journeyContext, {
|
|
@@ -25,10 +25,7 @@ const saveAndRedirect = (session, journeyContext, url, res, next) => {
|
|
|
25
25
|
});
|
|
26
26
|
};
|
|
27
27
|
|
|
28
|
-
export default ({
|
|
29
|
-
waypoint,
|
|
30
|
-
plan,
|
|
31
|
-
}) => [
|
|
28
|
+
export default ({ waypoint, plan }) => [
|
|
32
29
|
(req, res, next) => {
|
|
33
30
|
// Determine the next available waypoint after the current one
|
|
34
31
|
const traversed = plan.traverse(req.casa.journeyContext);
|
|
@@ -38,7 +35,9 @@ export default ({
|
|
|
38
35
|
Math.min(currentIndex + 1, traversed.length - 1),
|
|
39
36
|
);
|
|
40
37
|
const nextWaypoint = traversed[parseInt(nextIndex, 10)];
|
|
41
|
-
log.trace(
|
|
38
|
+
log.trace(
|
|
39
|
+
`currentIndex = ${currentIndex}, nextIndex = ${nextIndex}, currentWaypoint = ${waypoint}, nextWaypoint = ${nextWaypoint}`,
|
|
40
|
+
);
|
|
42
41
|
|
|
43
42
|
// Edit mode
|
|
44
43
|
// Attempt to take the user back to their original URL. We rely on the
|
|
@@ -55,14 +54,21 @@ export default ({
|
|
|
55
54
|
// they want to force the user to re-visit particular waypoints during this
|
|
56
55
|
// "jumping" phase.
|
|
57
56
|
if (req.casa.editMode && req.casa.editOrigin) {
|
|
58
|
-
const url = new URL(req.casa.editOrigin,
|
|
59
|
-
url.searchParams.append(
|
|
60
|
-
url.searchParams.append(
|
|
61
|
-
const redirectUrl =
|
|
57
|
+
const url = new URL(req.casa.editOrigin, "https://placeholder.test/");
|
|
58
|
+
url.searchParams.append("edit", "true");
|
|
59
|
+
url.searchParams.append("editorigin", req.casa.editOrigin);
|
|
60
|
+
const redirectUrl =
|
|
61
|
+
waypointUrl({ waypoint: url.pathname }) + url.search.toString();
|
|
62
62
|
|
|
63
63
|
log.debug(`Edit mode detected; redirecting to ${redirectUrl}`);
|
|
64
64
|
|
|
65
|
-
return saveAndRedirect(
|
|
65
|
+
return saveAndRedirect(
|
|
66
|
+
req.session,
|
|
67
|
+
req.casa.journeyContext,
|
|
68
|
+
redirectUrl,
|
|
69
|
+
res,
|
|
70
|
+
next,
|
|
71
|
+
);
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
// If the next URL is an "exit node", we need to flag that node as
|
|
@@ -76,7 +82,9 @@ export default ({
|
|
|
76
82
|
// setRoute('b', 'url:///otherapp/')
|
|
77
83
|
// setRoute('url:////otherapp/', 'c', (r, c) => checkIfOtherAppIsFinished())
|
|
78
84
|
if (Plan.isExitNode(nextWaypoint)) {
|
|
79
|
-
log.trace(
|
|
85
|
+
log.trace(
|
|
86
|
+
`Next waypoint is an exit node; clearing validation state on ${nextWaypoint}`,
|
|
87
|
+
);
|
|
80
88
|
req.casa.journeyContext.clearValidationErrorsForPage(nextWaypoint);
|
|
81
89
|
}
|
|
82
90
|
|
|
@@ -91,6 +99,12 @@ export default ({
|
|
|
91
99
|
|
|
92
100
|
// Save and move on
|
|
93
101
|
log.trace(`Redirecting to ${nextUrl}`);
|
|
94
|
-
return saveAndRedirect(
|
|
102
|
+
return saveAndRedirect(
|
|
103
|
+
req.session,
|
|
104
|
+
req.casa.journeyContext,
|
|
105
|
+
nextUrl,
|
|
106
|
+
res,
|
|
107
|
+
next,
|
|
108
|
+
);
|
|
95
109
|
},
|
|
96
110
|
];
|
|
@@ -2,19 +2,32 @@
|
|
|
2
2
|
// - Coerce each field to its correct type
|
|
3
3
|
// - Remove an extraneous fields that are not know to the application
|
|
4
4
|
|
|
5
|
-
import _ from
|
|
6
|
-
import fieldFactory from
|
|
7
|
-
import JourneyContext from
|
|
5
|
+
import _ from "lodash";
|
|
6
|
+
import fieldFactory from "../lib/field.js";
|
|
7
|
+
import JourneyContext from "../lib/JourneyContext.js";
|
|
8
8
|
|
|
9
|
-
export default ({
|
|
10
|
-
waypoint,
|
|
11
|
-
fields = [],
|
|
12
|
-
}) => {
|
|
9
|
+
export default ({ waypoint, fields = [] }) => {
|
|
13
10
|
// Add some common, transient fields to ensure they survive beyond this sanitisation process
|
|
14
|
-
fields.push(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
fields.push(
|
|
12
|
+
fieldFactory("_csrf", { persist: false }).processor((value) =>
|
|
13
|
+
String(value),
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
fields.push(
|
|
17
|
+
fieldFactory("contextid", { persist: false }).processor((value) =>
|
|
18
|
+
String(value),
|
|
19
|
+
),
|
|
20
|
+
);
|
|
21
|
+
fields.push(
|
|
22
|
+
fieldFactory("edit", { persist: false }).processor((value) =>
|
|
23
|
+
String(value),
|
|
24
|
+
),
|
|
25
|
+
);
|
|
26
|
+
fields.push(
|
|
27
|
+
fieldFactory("editorigin", { persist: false }).processor((value) =>
|
|
28
|
+
String(value),
|
|
29
|
+
),
|
|
30
|
+
);
|
|
18
31
|
|
|
19
32
|
// Middleware
|
|
20
33
|
return [
|
|
@@ -25,27 +38,37 @@ export default ({
|
|
|
25
38
|
/* eslint-disable security/detect-object-injection */
|
|
26
39
|
const prunedBody = Object.create(null);
|
|
27
40
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
28
|
-
if (
|
|
41
|
+
if (
|
|
42
|
+
_.has(req.body, fields[i].name) &&
|
|
43
|
+
req.body[fields[i].name] !== undefined
|
|
44
|
+
) {
|
|
29
45
|
prunedBody[fields[i].name] = req.body[fields[i].name];
|
|
30
46
|
}
|
|
31
47
|
}
|
|
32
48
|
/* eslint-enable security/detect-object-injection */
|
|
33
49
|
|
|
34
|
-
const journeyContext = JourneyContext.fromContext(
|
|
50
|
+
const journeyContext = JourneyContext.fromContext(
|
|
51
|
+
req.casa.journeyContext,
|
|
52
|
+
req,
|
|
53
|
+
);
|
|
35
54
|
journeyContext.setDataForPage(waypoint, prunedBody);
|
|
36
55
|
|
|
37
56
|
// Second, prune any fields that do not pass the validation conditional,
|
|
38
57
|
// and process those that do.
|
|
39
58
|
const sanitisedBody = Object.create(null);
|
|
40
59
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
41
|
-
const field =
|
|
60
|
+
const field =
|
|
61
|
+
fields[i]; /* eslint-disable-line security/detect-object-injection */
|
|
42
62
|
const fieldValue = field.getValue(prunedBody);
|
|
43
63
|
|
|
44
|
-
if (
|
|
45
|
-
fieldValue
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
if (
|
|
65
|
+
fieldValue !== undefined &&
|
|
66
|
+
field.testConditions({
|
|
67
|
+
fieldValue,
|
|
68
|
+
waypoint,
|
|
69
|
+
journeyContext,
|
|
70
|
+
})
|
|
71
|
+
) {
|
|
49
72
|
field.putValue(sanitisedBody, field.applyProcessors(fieldValue));
|
|
50
73
|
}
|
|
51
74
|
}
|
|
@@ -55,4 +78,4 @@ export default ({
|
|
|
55
78
|
next();
|
|
56
79
|
},
|
|
57
80
|
];
|
|
58
|
-
}
|
|
81
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
|
-
import { validateUrlPath } from
|
|
1
|
+
import { validateUrlPath } from "../lib/utils.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
+
* @typedef {import("express").RequestHandler} ExpressRequestHandler
|
|
4
5
|
* @access private
|
|
5
|
-
* @typedef {import('express').RequestHandler} ExpressRequestHandler
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
+
* @typedef {import("../casa").Plan} Plan
|
|
9
10
|
* @access private
|
|
10
|
-
* @typedef {import('../casa').Plan} Plan
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -17,12 +17,14 @@ import { validateUrlPath } from '../lib/utils.js';
|
|
|
17
17
|
* @param {Plan} plan CASA Plan
|
|
18
18
|
* @returns {ExpressRequestHandler[]} Array of middleware
|
|
19
19
|
*/
|
|
20
|
-
export default ({
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
20
|
+
export default ({ plan }) => [
|
|
21
|
+
(req, res) => {
|
|
22
|
+
const reqUrl = new URL(req.url, "https://placeholder.test/");
|
|
23
|
+
const reqPath = validateUrlPath(
|
|
24
|
+
`${req.baseUrl}${reqUrl.pathname}${plan.getWaypoints()[0]}`,
|
|
25
|
+
);
|
|
26
|
+
let reqParams = reqUrl.searchParams.toString();
|
|
27
|
+
reqParams = reqParams ? `?${reqParams}` : "";
|
|
28
|
+
res.redirect(302, `${reqPath}${reqParams}`);
|
|
29
|
+
},
|
|
30
|
+
];
|
|
@@ -1,64 +1,70 @@
|
|
|
1
1
|
// A last-modified cookie is used to control whether the end user sees a
|
|
2
2
|
// session-timeout page, or they are simply given a new session without
|
|
3
3
|
// interrupting their journey.
|
|
4
|
-
import expressSession, { MemoryStore } from
|
|
5
|
-
import logger from
|
|
6
|
-
import { validateUrlPath } from
|
|
4
|
+
import expressSession, { MemoryStore } from "express-session";
|
|
5
|
+
import logger from "../lib/logger.js";
|
|
6
|
+
import { validateUrlPath } from "../lib/utils.js";
|
|
7
7
|
|
|
8
|
-
const log = logger(
|
|
8
|
+
const log = logger("middleware:session");
|
|
9
9
|
|
|
10
|
-
const sessionExpiryMiddleware =
|
|
11
|
-
ttl,
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
removeCookie,
|
|
15
|
-
) => (req, res, next) => {
|
|
16
|
-
const lastModified = getCookie(req);
|
|
17
|
-
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
10
|
+
const sessionExpiryMiddleware =
|
|
11
|
+
(ttl, getCookie, touchCookie, removeCookie) => (req, res, next) => {
|
|
12
|
+
const lastModified = getCookie(req);
|
|
13
|
+
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
if (req.method === 'POST') {
|
|
31
|
-
log.info('The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.');
|
|
32
|
-
res.redirect(302, validateUrlPath(`${req.baseUrl}/`));
|
|
15
|
+
if (lastModified === 0) {
|
|
16
|
+
// New session, or grace period cookie no longer available after
|
|
17
|
+
// expiring; generate a new session, and create grace-period cookie.
|
|
18
|
+
// This will invalidate any CSRF tokens, so by letting the request POST
|
|
19
|
+
// requests through the user may see a 500 error response.
|
|
20
|
+
log.info(
|
|
21
|
+
"Session is new, or grace period has expired. Regenerating session.",
|
|
22
|
+
);
|
|
23
|
+
req.session.regenerate((err) => {
|
|
24
|
+
if (err) {
|
|
25
|
+
next(err);
|
|
33
26
|
} else {
|
|
34
|
-
|
|
27
|
+
touchCookie(res);
|
|
28
|
+
if (req.method === "POST") {
|
|
29
|
+
log.info(
|
|
30
|
+
"The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.",
|
|
31
|
+
);
|
|
32
|
+
res.redirect(302, validateUrlPath(`${req.baseUrl}/`));
|
|
33
|
+
} else {
|
|
34
|
+
next();
|
|
35
|
+
}
|
|
35
36
|
}
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
37
|
+
});
|
|
38
|
+
} else if (age > ttl) {
|
|
39
|
+
// Cookie has become stale and server session will have been removed;
|
|
40
|
+
// redirect to session-timeout
|
|
41
|
+
log.info(
|
|
42
|
+
"Session has timed out within grace period. Destroying session and redirecting to timeout page.",
|
|
43
|
+
);
|
|
44
|
+
const language = req.session.language ?? "en";
|
|
45
|
+
req.session.destroy((err) => {
|
|
46
|
+
if (err) {
|
|
47
|
+
next(err);
|
|
48
|
+
} else {
|
|
49
|
+
removeCookie(res);
|
|
50
|
+
const params = new URLSearchParams({
|
|
51
|
+
referrer: req.originalUrl,
|
|
52
|
+
lang: language,
|
|
53
|
+
});
|
|
54
|
+
/* eslint-disable-next-line prefer-template */
|
|
55
|
+
res.redirect(
|
|
56
|
+
302,
|
|
57
|
+
validateUrlPath(`${req.baseUrl}/session-timeout`) +
|
|
58
|
+
`?${params.toString()}`,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
} else {
|
|
63
|
+
// Touch cookie and continue
|
|
64
|
+
touchCookie(res);
|
|
65
|
+
next();
|
|
66
|
+
}
|
|
67
|
+
};
|
|
62
68
|
|
|
63
69
|
// 3 middleware:
|
|
64
70
|
// - set the session cookie
|
|
@@ -71,7 +77,7 @@ export default function sessionMiddleware({
|
|
|
71
77
|
secure,
|
|
72
78
|
ttl,
|
|
73
79
|
cookieSameSite = true,
|
|
74
|
-
cookiePath =
|
|
80
|
+
cookiePath = "/",
|
|
75
81
|
store = new MemoryStore(),
|
|
76
82
|
}) {
|
|
77
83
|
const commonCookieOptions = {
|
|
@@ -81,7 +87,8 @@ export default function sessionMiddleware({
|
|
|
81
87
|
};
|
|
82
88
|
|
|
83
89
|
if (cookieSameSite !== false) {
|
|
84
|
-
commonCookieOptions.sameSite =
|
|
90
|
+
commonCookieOptions.sameSite =
|
|
91
|
+
cookieSameSite === true ? "Strict" : cookieSameSite;
|
|
85
92
|
}
|
|
86
93
|
|
|
87
94
|
const ttlGrace = 1800; // user will see session-timeout if session expires within 30mins
|
|
@@ -94,22 +101,28 @@ export default function sessionMiddleware({
|
|
|
94
101
|
|
|
95
102
|
const getCookie = (req) => {
|
|
96
103
|
// Disabled eslint as `touchCookieName` is a constant, known value
|
|
97
|
-
|
|
98
|
-
|
|
104
|
+
const lastModified = Date.parse(
|
|
105
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
106
|
+
String(req.signedCookies[touchCookieName] ?? "1970-01-01T00:00:00+0000"),
|
|
107
|
+
);
|
|
99
108
|
return Number.isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001);
|
|
100
|
-
}
|
|
109
|
+
};
|
|
101
110
|
|
|
102
111
|
const touchCookie = (res) => {
|
|
103
112
|
// Touch cookie expiry is a short period after the session ttl. This gives
|
|
104
113
|
// a small period of time where a user will see the session-timeout message,
|
|
105
114
|
// which is important to avoid the confusion of simply being redirected back
|
|
106
115
|
// to the start of their journey.
|
|
107
|
-
res.cookie(
|
|
108
|
-
|
|
116
|
+
res.cookie(
|
|
117
|
+
touchCookieName,
|
|
118
|
+
new Date(Date.now()).toUTCString(),
|
|
119
|
+
touchCookieOptions,
|
|
120
|
+
);
|
|
121
|
+
};
|
|
109
122
|
|
|
110
123
|
const removeCookie = (res) => {
|
|
111
124
|
res.clearCookie(touchCookieName, touchCookieOptions);
|
|
112
|
-
}
|
|
125
|
+
};
|
|
113
126
|
|
|
114
127
|
return [
|
|
115
128
|
expressSession({
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
// Mark a waypoint as skipped
|
|
2
2
|
|
|
3
|
-
import lodash from
|
|
4
|
-
import JourneyContext from
|
|
5
|
-
import waypointUrl from
|
|
6
|
-
import logger from
|
|
3
|
+
import lodash from "lodash";
|
|
4
|
+
import JourneyContext from "../lib/JourneyContext.js";
|
|
5
|
+
import waypointUrl from "../lib/waypoint-url.js";
|
|
6
|
+
import logger from "../lib/logger.js";
|
|
7
7
|
|
|
8
8
|
const { has } = lodash;
|
|
9
9
|
|
|
10
|
-
const log = logger(
|
|
10
|
+
const log = logger("middleware:skip-waypoint");
|
|
11
11
|
|
|
12
|
-
export default ({
|
|
13
|
-
waypoint,
|
|
14
|
-
}) => [
|
|
12
|
+
export default ({ waypoint }) => [
|
|
15
13
|
(req, res, next) => {
|
|
16
|
-
if (!has(req.query,
|
|
14
|
+
if (!has(req.query, "skipto")) {
|
|
17
15
|
return next();
|
|
18
16
|
}
|
|
19
17
|
const skipTo = String(req.query.skipto);
|