@dwp/govuk-casa 8.16.2 → 8.16.4
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/dist/assets/css/casa-ie8.css +1 -1
- package/dist/assets/css/casa.css +1 -1
- package/dist/casa.d.ts +13 -13
- package/dist/casa.js +17 -7
- package/dist/casa.js.map +1 -1
- package/dist/lib/CasaTemplateLoader.d.ts +1 -1
- package/dist/lib/CasaTemplateLoader.js +13 -14
- package/dist/lib/CasaTemplateLoader.js.map +1 -1
- package/dist/lib/JourneyContext.d.ts +10 -4
- package/dist/lib/JourneyContext.js +57 -47
- package/dist/lib/JourneyContext.js.map +1 -1
- package/dist/lib/MutableRouter.d.ts +1 -1
- package/dist/lib/MutableRouter.js +22 -23
- package/dist/lib/MutableRouter.js.map +1 -1
- package/dist/lib/Plan.d.ts +5 -5
- package/dist/lib/Plan.js +49 -36
- package/dist/lib/Plan.js.map +1 -1
- package/dist/lib/ValidationError.d.ts +1 -1
- package/dist/lib/ValidationError.js +9 -9
- package/dist/lib/ValidationError.js.map +1 -1
- package/dist/lib/ValidatorFactory.js +4 -7
- package/dist/lib/ValidatorFactory.js.map +1 -1
- package/dist/lib/configuration-ingestor.d.ts +75 -14
- package/dist/lib/configuration-ingestor.js +156 -64
- package/dist/lib/configuration-ingestor.js.map +1 -1
- package/dist/lib/configure.js +11 -10
- package/dist/lib/configure.js.map +1 -1
- package/dist/lib/constants.js +8 -8
- package/dist/lib/context-id-generators.d.ts +1 -1
- package/dist/lib/context-id-generators.js +7 -4
- package/dist/lib/context-id-generators.js.map +1 -1
- package/dist/lib/end-session.js +2 -2
- package/dist/lib/field.d.ts +6 -6
- package/dist/lib/field.js +15 -21
- package/dist/lib/field.js.map +1 -1
- package/dist/lib/index.d.ts +13 -13
- package/dist/lib/index.js +17 -7
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/logger.js +7 -7
- package/dist/lib/logger.js.map +1 -1
- package/dist/lib/mount.js +3 -3
- package/dist/lib/mount.js.map +1 -1
- package/dist/lib/nunjucks-filters.d.ts +5 -1
- package/dist/lib/nunjucks-filters.js +37 -23
- package/dist/lib/nunjucks-filters.js.map +1 -1
- package/dist/lib/nunjucks.d.ts +2 -2
- package/dist/lib/nunjucks.js +6 -7
- package/dist/lib/nunjucks.js.map +1 -1
- package/dist/lib/utils.js +52 -42
- package/dist/lib/utils.js.map +1 -1
- package/dist/lib/validators/dateObject.d.ts +3 -3
- package/dist/lib/validators/dateObject.js +44 -37
- package/dist/lib/validators/dateObject.js.map +1 -1
- package/dist/lib/validators/email.d.ts +2 -2
- package/dist/lib/validators/email.js +4 -5
- package/dist/lib/validators/email.js.map +1 -1
- package/dist/lib/validators/inArray.d.ts +2 -2
- package/dist/lib/validators/inArray.js +5 -6
- package/dist/lib/validators/inArray.js.map +1 -1
- package/dist/lib/validators/index.d.ts +10 -10
- package/dist/lib/validators/index.js.map +1 -1
- package/dist/lib/validators/nino.d.ts +2 -2
- package/dist/lib/validators/nino.js +10 -7
- package/dist/lib/validators/nino.js.map +1 -1
- package/dist/lib/validators/postalAddressObject.d.ts +2 -2
- package/dist/lib/validators/postalAddressObject.js +52 -39
- package/dist/lib/validators/postalAddressObject.js.map +1 -1
- package/dist/lib/validators/range.d.ts +2 -2
- package/dist/lib/validators/range.js +6 -7
- package/dist/lib/validators/range.js.map +1 -1
- package/dist/lib/validators/regex.d.ts +2 -2
- package/dist/lib/validators/regex.js +4 -5
- package/dist/lib/validators/regex.js.map +1 -1
- package/dist/lib/validators/required.d.ts +2 -2
- package/dist/lib/validators/required.js +6 -9
- package/dist/lib/validators/required.js.map +1 -1
- package/dist/lib/validators/strlen.d.ts +2 -2
- package/dist/lib/validators/strlen.js +8 -9
- package/dist/lib/validators/strlen.js.map +1 -1
- package/dist/lib/validators/wordCount.d.ts +2 -2
- package/dist/lib/validators/wordCount.js +10 -9
- package/dist/lib/validators/wordCount.js.map +1 -1
- package/dist/lib/waypoint-url.d.ts +4 -4
- package/dist/lib/waypoint-url.js +23 -23
- package/dist/lib/waypoint-url.js.map +1 -1
- package/dist/middleware/body-parser.d.ts +27 -5
- package/dist/middleware/body-parser.js +37 -6
- package/dist/middleware/body-parser.js.map +1 -1
- package/dist/middleware/csrf.d.ts +3 -0
- package/dist/middleware/csrf.js +3 -0
- package/dist/middleware/csrf.js.map +1 -1
- package/dist/middleware/data.d.ts +22 -5
- package/dist/middleware/data.js +37 -7
- package/dist/middleware/data.js.map +1 -1
- package/dist/middleware/gather-fields.d.ts +1 -1
- package/dist/middleware/gather-fields.js +4 -3
- package/dist/middleware/gather-fields.js.map +1 -1
- package/dist/middleware/i18n.d.ts +11 -2
- package/dist/middleware/i18n.js +26 -17
- package/dist/middleware/i18n.js.map +1 -1
- package/dist/middleware/post.d.ts +3 -1
- package/dist/middleware/post.js +35 -18
- package/dist/middleware/post.js.map +1 -1
- package/dist/middleware/pre.d.ts +1 -1
- package/dist/middleware/pre.js +43 -21
- 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 +2 -2
- 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 +1 -1
- package/dist/middleware/serve-first-waypoint.js +6 -4
- package/dist/middleware/serve-first-waypoint.js.map +1 -1
- package/dist/middleware/session.d.ts +27 -8
- package/dist/middleware/session.js +53 -25
- 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 +15 -13
- 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 +2 -2
- package/dist/middleware/validate-fields.js +2 -5
- package/dist/middleware/validate-fields.js.map +1 -1
- package/dist/routes/ancillary.d.ts +2 -2
- package/dist/routes/ancillary.js +3 -3
- package/dist/routes/ancillary.js.map +1 -1
- package/dist/routes/journey.d.ts +1 -1
- package/dist/routes/journey.js +85 -31
- package/dist/routes/journey.js.map +1 -1
- package/dist/routes/static.d.ts +13 -4
- package/dist/routes/static.js +21 -19
- package/dist/routes/static.js.map +1 -1
- package/package.json +33 -36
- package/src/casa.js +13 -13
- package/src/lib/CasaTemplateLoader.js +21 -17
- package/src/lib/JourneyContext.js +118 -79
- package/src/lib/MutableRouter.js +30 -26
- package/src/lib/Plan.js +109 -62
- package/src/lib/ValidationError.js +13 -10
- package/src/lib/ValidatorFactory.js +7 -8
- package/src/lib/configuration-ingestor.js +200 -74
- package/src/lib/configure.js +31 -30
- package/src/lib/constants.js +8 -8
- package/src/lib/context-id-generators.js +39 -38
- package/src/lib/end-session.js +3 -3
- package/src/lib/field.js +48 -32
- package/src/lib/index.js +12 -12
- package/src/lib/logger.js +9 -9
- package/src/lib/mount.js +68 -73
- package/src/lib/nunjucks-filters.js +57 -44
- package/src/lib/nunjucks.js +20 -16
- package/src/lib/utils.js +69 -44
- package/src/lib/validators/dateObject.js +57 -48
- package/src/lib/validators/email.js +8 -9
- package/src/lib/validators/inArray.js +8 -9
- package/src/lib/validators/index.js +11 -11
- package/src/lib/validators/nino.js +25 -12
- package/src/lib/validators/postalAddressObject.js +73 -55
- package/src/lib/validators/range.js +9 -11
- package/src/lib/validators/regex.js +7 -8
- package/src/lib/validators/required.js +13 -14
- package/src/lib/validators/strlen.js +11 -12
- package/src/lib/validators/wordCount.js +17 -12
- package/src/lib/waypoint-url.js +48 -33
- package/src/middleware/body-parser.js +44 -10
- package/src/middleware/csrf.js +4 -1
- package/src/middleware/data.js +62 -25
- package/src/middleware/gather-fields.js +8 -8
- package/src/middleware/i18n.js +49 -39
- package/src/middleware/post.js +47 -21
- package/src/middleware/pre.js +59 -35
- package/src/middleware/progress-journey.js +32 -18
- package/src/middleware/sanitise-fields.js +43 -20
- package/src/middleware/serve-first-waypoint.js +12 -10
- package/src/middleware/session.js +97 -65
- package/src/middleware/skip-waypoint.js +7 -9
- package/src/middleware/steer-journey.js +39 -27
- package/src/middleware/strip-proxy-path.js +8 -7
- package/src/middleware/validate-fields.js +5 -12
- package/src/routes/ancillary.js +4 -6
- package/src/routes/journey.js +158 -78
- package/src/routes/static.js +64 -26
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
5
|
* @access private
|
|
6
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
|
|
@@ -34,14 +34,14 @@ function casaCspNonce(req, res) {
|
|
|
34
34
|
* @param {HelmetConfigurator} opts.helmetConfigurator Function to customise Helmet configuration
|
|
35
35
|
* @returns {Function[]} List of middleware
|
|
36
36
|
*/
|
|
37
|
-
export default ({
|
|
38
|
-
helmetConfigurator = (config) => (config),
|
|
39
|
-
} = {}) => [
|
|
37
|
+
export default ({ helmetConfigurator = (config) => config } = {}) => [
|
|
40
38
|
// Only allow certain request methods
|
|
41
39
|
(req, res, next) => {
|
|
42
|
-
if (req.method !==
|
|
43
|
-
const err = new Error(
|
|
44
|
-
|
|
40
|
+
if (req.method !== "GET" && req.method !== "POST") {
|
|
41
|
+
const err = new Error(
|
|
42
|
+
`Unaccepted request method, "${String(req.method).substr(0, 7)}"`,
|
|
43
|
+
);
|
|
44
|
+
err.code = "unaccepted_request_method";
|
|
45
45
|
next(err);
|
|
46
46
|
} else {
|
|
47
47
|
next();
|
|
@@ -53,39 +53,63 @@ export default ({
|
|
|
53
53
|
// The `no-store` setting is to specifically disable the bfcache and prevent
|
|
54
54
|
// possible leakage of information.
|
|
55
55
|
(req, res, next) => {
|
|
56
|
-
res.set(
|
|
57
|
-
res.set(
|
|
58
|
-
res.set(
|
|
59
|
-
res.set('x-robots-tag', 'noindex, nofollow');
|
|
56
|
+
res.set("cache-control", "no-cache, no-store, must-revalidate, private");
|
|
57
|
+
res.set("expires", 0);
|
|
58
|
+
res.set("x-robots-tag", "noindex, nofollow");
|
|
60
59
|
next();
|
|
61
60
|
},
|
|
62
61
|
|
|
63
62
|
// Generate nonces ready for use in Content-Security-Policy header and
|
|
64
63
|
// govuk-frontend template. This same none can be used wherever required.
|
|
65
64
|
(req, res, next) => {
|
|
66
|
-
res.locals.cspNonce = randomBytes(16).toString(
|
|
65
|
+
res.locals.cspNonce = randomBytes(16).toString("hex");
|
|
67
66
|
next();
|
|
68
67
|
},
|
|
69
68
|
|
|
70
69
|
// Helmet suite of headers
|
|
71
|
-
helmet(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
70
|
+
helmet(
|
|
71
|
+
helmetConfigurator({
|
|
72
|
+
// Allows GA which is typically used, and a known inline script nonce
|
|
73
|
+
contentSecurityPolicy: {
|
|
74
|
+
useDefaults: true,
|
|
75
|
+
directives: {
|
|
76
|
+
"default-src": ["'none'"],
|
|
77
|
+
"script-src": [
|
|
78
|
+
"'self'",
|
|
79
|
+
GA_DOMAIN,
|
|
80
|
+
GTM_DOMAIN,
|
|
81
|
+
GTM_PREVIEW_DOMAIN,
|
|
82
|
+
casaCspNonce,
|
|
83
|
+
],
|
|
84
|
+
"img-src": [
|
|
85
|
+
"'self'",
|
|
86
|
+
GA_DOMAIN,
|
|
87
|
+
GA_ANALYTICS_DOMAIN,
|
|
88
|
+
GTM_DOMAIN,
|
|
89
|
+
"https://ssl.gstatic.com",
|
|
90
|
+
"https://www.gstatic.com",
|
|
91
|
+
"https://fonts.gstatic.com",
|
|
92
|
+
],
|
|
93
|
+
"connect-src": ["'self'", GA_DOMAIN, GA_ANALYTICS_DOMAIN, GTM_DOMAIN],
|
|
94
|
+
"frame-src": ["'self'", GTM_DOMAIN],
|
|
95
|
+
"frame-ancestors": ["'self'"],
|
|
96
|
+
"form-action": ["'self'"],
|
|
97
|
+
"style-src": [
|
|
98
|
+
"'self'",
|
|
99
|
+
"https://fonts.googleapis.com",
|
|
100
|
+
GTM_PREVIEW_DOMAIN,
|
|
101
|
+
GTM_DOMAIN,
|
|
102
|
+
casaCspNonce,
|
|
103
|
+
"'sha256-xWGOGGMGQQ+IV0Om4xzgbDHXUh/+L1c375p0Pb6vF9A='",
|
|
104
|
+
"'sha256-9HGruJg4WccHXas5I1NmLn7tI1TDh6N26o6+/dy8sm4='",
|
|
105
|
+
"'sha256-oM0kKtU+nugIwjuYHkXXVoKGVNhC/DCUnIVdSVBMkaQ='",
|
|
106
|
+
],
|
|
107
|
+
"font-src": ["'self'", "data:", "https://fonts.gstatic.com"],
|
|
108
|
+
},
|
|
85
109
|
},
|
|
86
|
-
},
|
|
87
110
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
111
|
+
// // Require referrer to aid navigation
|
|
112
|
+
// referrerPolicy: 'no-referrer, same-origin',
|
|
113
|
+
}),
|
|
114
|
+
),
|
|
91
115
|
];
|
|
@@ -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,4 +1,4 @@
|
|
|
1
|
-
import { validateUrlPath } from
|
|
1
|
+
import { validateUrlPath } from "../lib/utils.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @access private
|
|
@@ -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,69 +1,94 @@
|
|
|
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
|
-
|
|
8
|
+
/**
|
|
9
|
+
* @typedef {import("express").RequestHandler} RequestHandler
|
|
10
|
+
* @access private
|
|
11
|
+
*/
|
|
9
12
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
touchCookie,
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
const lastModified = getCookie(req);
|
|
17
|
-
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
13
|
+
const log = logger("middleware:session");
|
|
14
|
+
|
|
15
|
+
const sessionExpiryMiddleware =
|
|
16
|
+
(ttl, getCookie, touchCookie, removeCookie) => (req, res, next) => {
|
|
17
|
+
const lastModified = getCookie(req);
|
|
18
|
+
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
18
19
|
|
|
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}/`));
|
|
20
|
+
if (lastModified === 0) {
|
|
21
|
+
// New session, or grace period cookie no longer available after
|
|
22
|
+
// expiring; generate a new session, and create grace-period cookie.
|
|
23
|
+
// This will invalidate any CSRF tokens, so by letting the request POST
|
|
24
|
+
// requests through the user may see a 500 error response.
|
|
25
|
+
log.info(
|
|
26
|
+
"Session is new, or grace period has expired. Regenerating session.",
|
|
27
|
+
);
|
|
28
|
+
req.session.regenerate((err) => {
|
|
29
|
+
if (err) {
|
|
30
|
+
next(err);
|
|
33
31
|
} else {
|
|
34
|
-
|
|
32
|
+
touchCookie(res);
|
|
33
|
+
if (req.method === "POST") {
|
|
34
|
+
log.info(
|
|
35
|
+
"The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.",
|
|
36
|
+
);
|
|
37
|
+
res.redirect(302, validateUrlPath(`${req.baseUrl}/`));
|
|
38
|
+
} else {
|
|
39
|
+
next();
|
|
40
|
+
}
|
|
35
41
|
}
|
|
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
|
-
}
|
|
42
|
+
});
|
|
43
|
+
} else if (age > ttl) {
|
|
44
|
+
// Cookie has become stale and server session will have been removed;
|
|
45
|
+
// redirect to session-timeout
|
|
46
|
+
log.info(
|
|
47
|
+
"Session has timed out within grace period. Destroying session and redirecting to timeout page.",
|
|
48
|
+
);
|
|
49
|
+
const language = req.session.language ?? "en";
|
|
50
|
+
req.session.destroy((err) => {
|
|
51
|
+
if (err) {
|
|
52
|
+
next(err);
|
|
53
|
+
} else {
|
|
54
|
+
removeCookie(res);
|
|
55
|
+
const params = new URLSearchParams({
|
|
56
|
+
referrer: req.originalUrl,
|
|
57
|
+
lang: language,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
res.redirect(
|
|
61
|
+
302,
|
|
62
|
+
validateUrlPath(`${req.baseUrl}/session-timeout`) +
|
|
63
|
+
`?${params.toString()}`,
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
} else {
|
|
68
|
+
// Touch cookie and continue
|
|
69
|
+
touchCookie(res);
|
|
70
|
+
next();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
62
73
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
74
|
+
/**
|
|
75
|
+
* Produces three middleware functions:
|
|
76
|
+
*
|
|
77
|
+
* - Set the session cookie
|
|
78
|
+
* - Parse request cookies
|
|
79
|
+
* - Handle expiry of server-side session
|
|
80
|
+
*
|
|
81
|
+
* @param {object} opts Options
|
|
82
|
+
* @param {RequestHandler} opts.cookieParserMiddleware Cookie parsing middleware
|
|
83
|
+
* @param {string} opts.secret Session encryption secret
|
|
84
|
+
* @param {string} opts.name Session cookie name
|
|
85
|
+
* @param {boolean} opts.secure Secure cookies only
|
|
86
|
+
* @param {number} opts.ttl Session data time-to-live
|
|
87
|
+
* @param {boolean | string} [opts.cookieSameSite] Cooke SameSite setting
|
|
88
|
+
* @param {string} [opts.cookiePath] Cookie path
|
|
89
|
+
* @param {object} [opts.store] Storage instance
|
|
90
|
+
* @returns {RequestHandler[]} Middleware functions
|
|
91
|
+
*/
|
|
67
92
|
export default function sessionMiddleware({
|
|
68
93
|
cookieParserMiddleware,
|
|
69
94
|
secret,
|
|
@@ -71,7 +96,7 @@ export default function sessionMiddleware({
|
|
|
71
96
|
secure,
|
|
72
97
|
ttl,
|
|
73
98
|
cookieSameSite = true,
|
|
74
|
-
cookiePath =
|
|
99
|
+
cookiePath = "/",
|
|
75
100
|
store = new MemoryStore(),
|
|
76
101
|
}) {
|
|
77
102
|
const commonCookieOptions = {
|
|
@@ -81,7 +106,8 @@ export default function sessionMiddleware({
|
|
|
81
106
|
};
|
|
82
107
|
|
|
83
108
|
if (cookieSameSite !== false) {
|
|
84
|
-
commonCookieOptions.sameSite =
|
|
109
|
+
commonCookieOptions.sameSite =
|
|
110
|
+
cookieSameSite === true ? "Strict" : cookieSameSite;
|
|
85
111
|
}
|
|
86
112
|
|
|
87
113
|
const ttlGrace = 1800; // user will see session-timeout if session expires within 30mins
|
|
@@ -94,22 +120,28 @@ export default function sessionMiddleware({
|
|
|
94
120
|
|
|
95
121
|
const getCookie = (req) => {
|
|
96
122
|
// Disabled eslint as `touchCookieName` is a constant, known value
|
|
97
|
-
|
|
98
|
-
const lastModified = Date.parse(
|
|
123
|
+
|
|
124
|
+
const lastModified = Date.parse(
|
|
125
|
+
String(req.signedCookies[touchCookieName] ?? "1970-01-01T00:00:00+0000"),
|
|
126
|
+
);
|
|
99
127
|
return Number.isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001);
|
|
100
|
-
}
|
|
128
|
+
};
|
|
101
129
|
|
|
102
130
|
const touchCookie = (res) => {
|
|
103
131
|
// Touch cookie expiry is a short period after the session ttl. This gives
|
|
104
132
|
// a small period of time where a user will see the session-timeout message,
|
|
105
133
|
// which is important to avoid the confusion of simply being redirected back
|
|
106
134
|
// to the start of their journey.
|
|
107
|
-
res.cookie(
|
|
108
|
-
|
|
135
|
+
res.cookie(
|
|
136
|
+
touchCookieName,
|
|
137
|
+
new Date(Date.now()).toUTCString(),
|
|
138
|
+
touchCookieOptions,
|
|
139
|
+
);
|
|
140
|
+
};
|
|
109
141
|
|
|
110
142
|
const removeCookie = (res) => {
|
|
111
143
|
res.clearCookie(touchCookieName, touchCookieOptions);
|
|
112
|
-
}
|
|
144
|
+
};
|
|
113
145
|
|
|
114
146
|
return [
|
|
115
147
|
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);
|