@dwp/govuk-casa 8.7.9 → 8.8.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 +0 -5
- package/dist/casa.d.ts +1 -1
- package/dist/casa.js +3 -2
- package/dist/casa.js.map +1 -0
- package/dist/lib/CasaTemplateLoader.d.ts +1 -7
- package/dist/lib/CasaTemplateLoader.js +6 -1
- package/dist/lib/CasaTemplateLoader.js.map +1 -0
- package/dist/lib/JourneyContext.d.ts +1 -1
- package/dist/lib/JourneyContext.js +2 -1
- package/dist/lib/JourneyContext.js.map +1 -0
- package/dist/lib/MutableRouter.js +1 -0
- package/dist/lib/MutableRouter.js.map +1 -0
- package/dist/lib/Plan.d.ts +2 -0
- package/dist/lib/Plan.js +9 -4
- package/dist/lib/Plan.js.map +1 -0
- package/dist/lib/ValidationError.js +2 -1
- package/dist/lib/ValidationError.js.map +1 -0
- package/dist/lib/ValidatorFactory.d.ts +2 -2
- package/dist/lib/ValidatorFactory.js +3 -2
- package/dist/lib/ValidatorFactory.js.map +1 -0
- package/dist/lib/configuration-ingestor.d.ts +1 -1
- package/dist/lib/configuration-ingestor.js +2 -1
- package/dist/lib/configuration-ingestor.js.map +1 -0
- package/dist/lib/configure.js +2 -1
- package/dist/lib/configure.js.map +1 -0
- package/dist/lib/end-session.js +1 -0
- package/dist/lib/end-session.js.map +1 -0
- package/dist/lib/field.js +1 -0
- package/dist/lib/field.js.map +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/index.js.map +1 -0
- package/dist/lib/logger.js +1 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/mount.js +3 -2
- package/dist/lib/mount.js.map +1 -0
- package/dist/lib/nunjucks-filters.js +28 -1
- package/dist/lib/nunjucks-filters.js.map +1 -0
- package/dist/lib/nunjucks.js +1 -0
- package/dist/lib/nunjucks.js.map +1 -0
- package/dist/lib/utils.d.ts +46 -28
- package/dist/lib/utils.js +105 -67
- package/dist/lib/utils.js.map +1 -0
- package/dist/lib/validators/dateObject.js +5 -4
- package/dist/lib/validators/dateObject.js.map +1 -0
- package/dist/lib/validators/email.js +1 -0
- package/dist/lib/validators/email.js.map +1 -0
- package/dist/lib/validators/inArray.js +1 -0
- package/dist/lib/validators/inArray.js.map +1 -0
- package/dist/lib/validators/index.js +1 -0
- package/dist/lib/validators/index.js.map +1 -0
- package/dist/lib/validators/nino.js +1 -0
- package/dist/lib/validators/nino.js.map +1 -0
- package/dist/lib/validators/postalAddressObject.d.ts +2 -2
- package/dist/lib/validators/postalAddressObject.js +2 -1
- package/dist/lib/validators/postalAddressObject.js.map +1 -0
- package/dist/lib/validators/regex.js +1 -0
- package/dist/lib/validators/regex.js.map +1 -0
- package/dist/lib/validators/required.d.ts +1 -1
- package/dist/lib/validators/required.js +2 -1
- package/dist/lib/validators/required.js.map +1 -0
- package/dist/lib/validators/strlen.js +1 -0
- package/dist/lib/validators/strlen.js.map +1 -0
- package/dist/lib/validators/wordCount.js +1 -0
- package/dist/lib/validators/wordCount.js.map +1 -0
- package/dist/lib/waypoint-url.js +1 -0
- package/dist/lib/waypoint-url.js.map +1 -0
- package/dist/middleware/body-parser.js +1 -0
- package/dist/middleware/body-parser.js.map +1 -0
- package/dist/middleware/csrf.js +1 -0
- package/dist/middleware/csrf.js.map +1 -0
- package/dist/middleware/data.js +1 -0
- package/dist/middleware/data.js.map +1 -0
- package/dist/middleware/gather-fields.js +2 -1
- package/dist/middleware/gather-fields.js.map +1 -0
- package/dist/middleware/i18n.js +1 -0
- package/dist/middleware/i18n.js.map +1 -0
- package/dist/middleware/post.js +1 -0
- package/dist/middleware/post.js.map +1 -0
- package/dist/middleware/pre.js +1 -0
- package/dist/middleware/pre.js.map +1 -0
- package/dist/middleware/progress-journey.js +1 -0
- package/dist/middleware/progress-journey.js.map +1 -0
- package/dist/middleware/sanitise-fields.js +1 -0
- package/dist/middleware/sanitise-fields.js.map +1 -0
- package/dist/middleware/serve-first-waypoint.js +1 -0
- package/dist/middleware/serve-first-waypoint.js.map +1 -0
- package/dist/middleware/session.js +1 -0
- package/dist/middleware/session.js.map +1 -0
- package/dist/middleware/skip-waypoint.js +1 -0
- package/dist/middleware/skip-waypoint.js.map +1 -0
- package/dist/middleware/steer-journey.js +1 -0
- package/dist/middleware/steer-journey.js.map +1 -0
- package/dist/middleware/strip-proxy-path.js +2 -1
- package/dist/middleware/strip-proxy-path.js.map +1 -0
- package/dist/middleware/validate-fields.js +2 -1
- package/dist/middleware/validate-fields.js.map +1 -0
- package/dist/mjs/esm-wrapper.js +10 -15
- package/dist/routes/ancillary.js +1 -0
- package/dist/routes/ancillary.js.map +1 -0
- package/dist/routes/journey.js +1 -0
- package/dist/routes/journey.js.map +1 -0
- package/dist/routes/static.js +1 -0
- package/dist/routes/static.js.map +1 -0
- package/locales/cy/error.json +1 -1
- package/locales/en/error.json +1 -1
- package/package.json +20 -19
- package/src/casa.js +320 -0
- package/src/lib/CasaTemplateLoader.js +104 -0
- package/src/lib/JourneyContext.js +783 -0
- package/src/lib/MutableRouter.js +310 -0
- package/src/lib/Plan.js +624 -0
- package/src/lib/ValidationError.js +163 -0
- package/src/lib/ValidatorFactory.js +105 -0
- package/src/lib/configuration-ingestor.js +457 -0
- package/src/lib/configure.js +202 -0
- package/src/lib/dirname.cjs +1 -0
- package/src/lib/end-session.js +45 -0
- package/src/lib/field.js +456 -0
- package/src/lib/index.js +33 -0
- package/src/lib/logger.js +16 -0
- package/src/lib/mount.js +127 -0
- package/src/lib/nunjucks-filters.js +150 -0
- package/src/lib/nunjucks.js +53 -0
- package/src/lib/utils.js +232 -0
- package/src/lib/validators/dateObject.js +169 -0
- package/src/lib/validators/email.js +55 -0
- package/src/lib/validators/inArray.js +81 -0
- package/src/lib/validators/index.js +24 -0
- package/src/lib/validators/nino.js +57 -0
- package/src/lib/validators/postalAddressObject.js +162 -0
- package/src/lib/validators/regex.js +48 -0
- package/src/lib/validators/required.js +74 -0
- package/src/lib/validators/strlen.js +66 -0
- package/src/lib/validators/wordCount.js +70 -0
- package/src/lib/waypoint-url.js +93 -0
- package/src/middleware/body-parser.js +31 -0
- package/src/middleware/csrf.js +29 -0
- package/src/middleware/data.js +105 -0
- package/src/middleware/dirname.cjs +1 -0
- package/src/middleware/gather-fields.js +51 -0
- package/src/middleware/i18n.js +106 -0
- package/src/middleware/post.js +61 -0
- package/src/middleware/pre.js +91 -0
- package/src/middleware/progress-journey.js +92 -0
- package/src/middleware/sanitise-fields.js +58 -0
- package/src/middleware/serve-first-waypoint.js +28 -0
- package/src/middleware/session.js +129 -0
- package/src/middleware/skip-waypoint.js +46 -0
- package/src/middleware/steer-journey.js +78 -0
- package/src/middleware/strip-proxy-path.js +56 -0
- package/src/middleware/validate-fields.js +84 -0
- package/src/routes/ancillary.js +29 -0
- package/src/routes/dirname.cjs +1 -0
- package/src/routes/journey.js +212 -0
- package/src/routes/static.js +77 -0
- package/views/casa/components/character-count/README.md +10 -0
- package/views/casa/components/character-count/template.njk +6 -2
- package/views/casa/components/checkboxes/README.md +45 -37
- package/views/casa/components/checkboxes/template.njk +8 -7
- package/views/casa/components/date-input/README.md +15 -3
- package/views/casa/components/date-input/template.njk +6 -4
- package/views/casa/components/input/README.md +9 -0
- package/views/casa/components/input/template.njk +6 -2
- package/views/casa/components/journey-form/README.md +3 -2
- package/views/casa/components/postal-address-object/README.md +10 -0
- package/views/casa/components/postal-address-object/template.njk +20 -5
- package/views/casa/components/radios/README.md +51 -26
- package/views/casa/components/radios/template.njk +6 -3
- package/views/casa/components/select/README.md +65 -0
- package/views/casa/components/select/macro.njk +3 -0
- package/views/casa/components/select/template.njk +49 -0
- package/views/casa/components/textarea/README.md +9 -0
- package/views/casa/components/textarea/template.njk +6 -2
- package/views/casa/layouts/journey.njk +1 -1
- package/views/casa/layouts/main.njk +1 -1
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
/* eslint-disable sonarjs/no-duplicate-string */
|
|
2
|
+
import { PageField } from './field.js';
|
|
3
|
+
import Plan from './Plan.js';
|
|
4
|
+
import logger from './logger.js';
|
|
5
|
+
import {
|
|
6
|
+
validateWaypoint,
|
|
7
|
+
validateHookName,
|
|
8
|
+
validateHookPath,
|
|
9
|
+
validateView,
|
|
10
|
+
} from './utils.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @access private
|
|
14
|
+
* @typedef {import('../casa').ConfigurationOptions} ConfigurationOptions
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @access private
|
|
19
|
+
* @typedef {import('../casa').HelmetConfigurator} HelmetConfigurator
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
const log = logger('lib:configuration-ingestor');
|
|
23
|
+
|
|
24
|
+
const echo = (a) => (a);
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validates and sanitises i18n object.
|
|
28
|
+
*
|
|
29
|
+
* @access private
|
|
30
|
+
* @param {object} i18n Object to validate.
|
|
31
|
+
* @param {Function} cb Callback function that receives the validated value.
|
|
32
|
+
* @throws {TypeError} For invalid object.
|
|
33
|
+
* @returns {object} Sanitised i18n object.
|
|
34
|
+
*/
|
|
35
|
+
export function validateI18nObject(i18n = Object.create(null), cb = echo) {
|
|
36
|
+
if (Object.prototype.toString.call(i18n) !== '[object Object]') {
|
|
37
|
+
throw new TypeError('I18n must be an object');
|
|
38
|
+
}
|
|
39
|
+
return cb(i18n);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validates and sanitises i18n directory.
|
|
44
|
+
*
|
|
45
|
+
* @access private
|
|
46
|
+
* @param {Array} dirs Array of directories.
|
|
47
|
+
* @throws {SyntaxError} For invalid directories.
|
|
48
|
+
* @throws {TypeError} For invalid type.
|
|
49
|
+
* @returns {Array} Array of directories.
|
|
50
|
+
*/
|
|
51
|
+
export function validateI18nDirs(dirs = []) {
|
|
52
|
+
if (!Array.isArray(dirs)) {
|
|
53
|
+
throw new TypeError('I18n directories must be an array (i18n.dirs)');
|
|
54
|
+
}
|
|
55
|
+
dirs.forEach((dir, i) => {
|
|
56
|
+
if (typeof dir !== 'string') {
|
|
57
|
+
throw new TypeError(`I18n directory must be a string, got ${typeof dir} (i18n.dirs[${i}])`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return dirs;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validates and sanitises i18n locales.
|
|
65
|
+
*
|
|
66
|
+
* @access private
|
|
67
|
+
* @param {Array} locales Array of locales.
|
|
68
|
+
* @throws {SyntaxError} For invalid locales.
|
|
69
|
+
* @throws {TypeError} For invalid type.
|
|
70
|
+
* @returns {Array} Array of locales.
|
|
71
|
+
*/
|
|
72
|
+
export function validateI18nLocales(locales = ['en', 'cy']) {
|
|
73
|
+
if (!Array.isArray(locales)) {
|
|
74
|
+
throw new TypeError('I18n locales must be an array (i18n.locales)');
|
|
75
|
+
}
|
|
76
|
+
locales.forEach((locale, i) => {
|
|
77
|
+
if (typeof locale !== 'string') {
|
|
78
|
+
throw new TypeError(`I18n locale must be a string, got ${typeof locale} (i18n.locales[${i}])`);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
return locales;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Validates and sanitises mount url.
|
|
86
|
+
*
|
|
87
|
+
* @access private
|
|
88
|
+
* @param {string} mountUrl Prefix for all URLs in the browser address bar
|
|
89
|
+
* @throws {SyntaxError} For invalid URL.
|
|
90
|
+
* @returns {string|undefined} Sanitised URL.
|
|
91
|
+
*/
|
|
92
|
+
export function validateMountUrl(mountUrl) {
|
|
93
|
+
if (typeof mountUrl === 'undefined') {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
if (!mountUrl.match(/\/$/)) {
|
|
97
|
+
throw new SyntaxError('mountUrl must include a trailing slash (/)');
|
|
98
|
+
}
|
|
99
|
+
return mountUrl;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validates and sanitises sessions object.
|
|
104
|
+
*
|
|
105
|
+
* @access private
|
|
106
|
+
* @param {object} session Object to validate.
|
|
107
|
+
* @param {Function} cb Callback function that receives the validated value.
|
|
108
|
+
* @throws {TypeError} For invalid object.
|
|
109
|
+
* @returns {object} Sanitised sessions object.
|
|
110
|
+
*/
|
|
111
|
+
export function validateSessionObject(session = Object.create(null), cb = echo) {
|
|
112
|
+
if (session === undefined) {
|
|
113
|
+
return cb(session);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof session !== 'object') {
|
|
117
|
+
throw new TypeError('Session config has not been specified');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return cb(session);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validates and sanitises view directory.
|
|
125
|
+
*
|
|
126
|
+
* @access private
|
|
127
|
+
* @param {Array} dirs Array of directories.
|
|
128
|
+
* @throws {SyntaxError} For invalid directories.
|
|
129
|
+
* @throws {TypeError} For invalid type.
|
|
130
|
+
* @returns {Array} Array of directories.
|
|
131
|
+
*/
|
|
132
|
+
export function validateViews(dirs = []) {
|
|
133
|
+
if (!Array.isArray(dirs)) {
|
|
134
|
+
throw new TypeError('View directories must be an array (views)');
|
|
135
|
+
}
|
|
136
|
+
dirs.forEach((dir, i) => {
|
|
137
|
+
if (typeof dir !== 'string') {
|
|
138
|
+
throw new TypeError(`View directory must be a string, got ${typeof dir} (views[${i}])`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
return dirs;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validates and sanitises sessions secret.
|
|
146
|
+
*
|
|
147
|
+
* @access private
|
|
148
|
+
* @param {string} secret Session secret.
|
|
149
|
+
* @throws {ReferenceError} For missing value type.
|
|
150
|
+
* @throws {TypeError} For invalid value.
|
|
151
|
+
* @returns {string} Secret.
|
|
152
|
+
*/
|
|
153
|
+
export function validateSessionSecret(secret) {
|
|
154
|
+
if (typeof secret === 'undefined') {
|
|
155
|
+
throw ReferenceError('Session secret is missing (session.secret)')
|
|
156
|
+
} else if (typeof secret !== 'string') {
|
|
157
|
+
throw new TypeError('Session secret must be a string (session.secret)');
|
|
158
|
+
}
|
|
159
|
+
return secret;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Validates and sanitises sessions ttl.
|
|
164
|
+
*
|
|
165
|
+
* @access private
|
|
166
|
+
* @param {number} ttl Session ttl (seconds).
|
|
167
|
+
* @throws {ReferenceError} For missing value type.
|
|
168
|
+
* @throws {TypeError} For invalid value.
|
|
169
|
+
* @returns {number} Ttl.
|
|
170
|
+
*/
|
|
171
|
+
export function validateSessionTtl(ttl = 3600) {
|
|
172
|
+
if (typeof ttl !== 'number') {
|
|
173
|
+
throw new TypeError('Session ttl must be an integer (session.ttl)');
|
|
174
|
+
}
|
|
175
|
+
return ttl;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Validates and sanitises sessions name.
|
|
180
|
+
*
|
|
181
|
+
* @access private
|
|
182
|
+
* @param {string} [name=casa-session] Session name.
|
|
183
|
+
* @throws {ReferenceError} For missing value type.
|
|
184
|
+
* @throws {TypeError} For invalid value.
|
|
185
|
+
* @returns {string} Name.
|
|
186
|
+
*/
|
|
187
|
+
export function validateSessionName(name = 'casa-session') {
|
|
188
|
+
if (typeof name !== 'string') {
|
|
189
|
+
throw new TypeError('Session name must be a string (session.name)');
|
|
190
|
+
}
|
|
191
|
+
return name;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Validates and sanitises sessions secure flag.
|
|
196
|
+
*
|
|
197
|
+
* @access private
|
|
198
|
+
* @param {boolean} [secure] Session secure flag.
|
|
199
|
+
* @throws {ReferenceError} For missing value type.
|
|
200
|
+
* @throws {TypeError} For invalid or missing value.
|
|
201
|
+
* @returns {string} Name.
|
|
202
|
+
*/
|
|
203
|
+
export function validateSessionSecure(secure) {
|
|
204
|
+
if (secure === undefined) {
|
|
205
|
+
throw new Error('Session secure flag must be explicitly defined (session.secure)');
|
|
206
|
+
}
|
|
207
|
+
if (typeof secure !== 'boolean') {
|
|
208
|
+
throw new TypeError('Session secure flag must be boolean (session.secure)');
|
|
209
|
+
}
|
|
210
|
+
return secure;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Validates and sanitises sessions store.
|
|
215
|
+
*
|
|
216
|
+
* @access private
|
|
217
|
+
* @param {Function} store Session store.
|
|
218
|
+
* @returns {Function} Store.
|
|
219
|
+
*/
|
|
220
|
+
export function validateSessionStore(store) {
|
|
221
|
+
if (typeof store === 'undefined') {
|
|
222
|
+
log.warn('Using MemoryStore session storage, which is not suitable for production');
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
return store;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Validates and sanitises sessions cookie url path.
|
|
230
|
+
*
|
|
231
|
+
* @access private
|
|
232
|
+
* @param {string} cookiePath Session cookie url path.
|
|
233
|
+
* @param {string} defaultPath Default path if none specified.
|
|
234
|
+
* @returns {string} Cookie path.
|
|
235
|
+
*/
|
|
236
|
+
export function validateSessionCookiePath(cookiePath, defaultPath = '/') {
|
|
237
|
+
if (typeof cookiePath === 'undefined') {
|
|
238
|
+
return defaultPath;
|
|
239
|
+
}
|
|
240
|
+
return cookiePath;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Validates and sanitises sessions cookie "sameSite" flag. One of:
|
|
245
|
+
* true (Strict)
|
|
246
|
+
* false (will not set the flag at all)
|
|
247
|
+
* Strict
|
|
248
|
+
* Lax
|
|
249
|
+
* None
|
|
250
|
+
*
|
|
251
|
+
* @access private
|
|
252
|
+
* @param {any} cookieSameSite Session cookie "sameSite" flag
|
|
253
|
+
* @param {any} defaultFlag Default path if none specified
|
|
254
|
+
* @returns {boolean} cookie path
|
|
255
|
+
* @throws {TypeError} When invalid arguments are provided
|
|
256
|
+
*/
|
|
257
|
+
export function validateSessionCookieSameSite(cookieSameSite, defaultFlag) {
|
|
258
|
+
const validValues = [true, false, 'Strict', 'Lax', 'None'];
|
|
259
|
+
|
|
260
|
+
if (defaultFlag === undefined) {
|
|
261
|
+
throw new TypeError('validateSessionCookieSameSite() requires an explicit default flag');
|
|
262
|
+
} else if (!validValues.includes(defaultFlag)) {
|
|
263
|
+
throw new TypeError('validateSessionCookieSameSite() default flag must be set to one of true, false, Strict, Lax or None (session.cookieSameSite)');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const value = cookieSameSite !== undefined ? cookieSameSite : defaultFlag;
|
|
267
|
+
if (!validValues.includes(value)) {
|
|
268
|
+
throw new TypeError('SameSite flag must be set to one of true, false, Strict, Lax or None (session.cookieSameSite)');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return value;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const validatePageHook = (hook, index) => {
|
|
275
|
+
try {
|
|
276
|
+
validateHookName(hook.hook);
|
|
277
|
+
if (typeof hook.middleware !== 'function') {
|
|
278
|
+
throw new TypeError('Hook middleware must be a function');
|
|
279
|
+
}
|
|
280
|
+
} catch (err) {
|
|
281
|
+
err.message = `Page hook at index ${index} is invalid: ${err.message}`;
|
|
282
|
+
throw err;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
export function validatePageHooks(hooks) {
|
|
287
|
+
if (!Array.isArray(hooks)) {
|
|
288
|
+
throw new TypeError('Hooks must be an array');
|
|
289
|
+
}
|
|
290
|
+
hooks.forEach((hook, index) => validatePageHook(hook, index));
|
|
291
|
+
return hooks;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const validateField = (field, index) => {
|
|
295
|
+
try {
|
|
296
|
+
if (!(field instanceof PageField)) {
|
|
297
|
+
throw new TypeError('Page field must be an instance of PageField (created via the "field()" function)');
|
|
298
|
+
}
|
|
299
|
+
} catch (err) {
|
|
300
|
+
err.message = `Page field at index ${index} is invalid: ${err.message}`;
|
|
301
|
+
throw err;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
export function validateFields(fields) {
|
|
306
|
+
if (!Array.isArray(fields)) {
|
|
307
|
+
throw new TypeError('Page fields must be an array (page[].fields)');
|
|
308
|
+
}
|
|
309
|
+
fields.forEach((hook, index) => validateField(hook, index));
|
|
310
|
+
return fields;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const validatePage = (page, index) => {
|
|
314
|
+
try {
|
|
315
|
+
validateWaypoint(page.waypoint);
|
|
316
|
+
validateView(page.view);
|
|
317
|
+
if (page.fields !== undefined) {
|
|
318
|
+
validateFields(page.fields);
|
|
319
|
+
}
|
|
320
|
+
if (page.hooks !== undefined) {
|
|
321
|
+
validatePageHooks(page.hooks);
|
|
322
|
+
}
|
|
323
|
+
} catch (err) {
|
|
324
|
+
err.message = `Page at index ${index} is invalid: ${err.message}`;
|
|
325
|
+
throw err;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function validatePages(pages = []) {
|
|
330
|
+
if (!Array.isArray(pages)) {
|
|
331
|
+
throw new TypeError('Pages must be an array (pages)');
|
|
332
|
+
}
|
|
333
|
+
pages.forEach((page, index) => validatePage(page, index));
|
|
334
|
+
return pages;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export function validatePlan(plan) {
|
|
338
|
+
if (plan === undefined) {
|
|
339
|
+
return plan;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
if (!(plan instanceof Plan)) {
|
|
343
|
+
throw new TypeError('Plan must be an instance the Plan class (plan)');
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return plan;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const validateGlobalHook = (hook, index) => {
|
|
350
|
+
try {
|
|
351
|
+
validateHookName(hook.hook);
|
|
352
|
+
if (typeof hook.middleware !== 'function') {
|
|
353
|
+
throw new TypeError('Hook middleware must be a function');
|
|
354
|
+
}
|
|
355
|
+
if (hook.path !== undefined) {
|
|
356
|
+
validateHookPath(hook.path);
|
|
357
|
+
}
|
|
358
|
+
} catch (err) {
|
|
359
|
+
err.message = `Global hook at index ${index} is invalid: ${err.message}`;
|
|
360
|
+
throw err;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
export function validateGlobalHooks(hooks) {
|
|
365
|
+
if (hooks === undefined) {
|
|
366
|
+
return [];
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (!Array.isArray(hooks)) {
|
|
370
|
+
throw new TypeError('Hooks must be an array');
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
hooks.forEach((hook, index) => validateGlobalHook(hook, index));
|
|
374
|
+
return hooks;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export function validatePlugins(plugins) {
|
|
378
|
+
return plugins;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
export function validateEvents(events) {
|
|
382
|
+
return events;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Validates helmet configuration function.
|
|
387
|
+
*
|
|
388
|
+
* @access private
|
|
389
|
+
* @param {HelmetConfigurator} helmetConfigurator Configuration function
|
|
390
|
+
* @returns {HelmetConfigurator} Validated configuration function
|
|
391
|
+
* @throws {TypeError} when passed a non-function
|
|
392
|
+
*/
|
|
393
|
+
export function validateHelmetConfigurator(helmetConfigurator) {
|
|
394
|
+
if (helmetConfigurator !== undefined && !(helmetConfigurator instanceof Function)) {
|
|
395
|
+
throw new TypeError('Helmet configurator must be a function');
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return helmetConfigurator;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Ingest, validate, sanitise and manipulate configuration parameters.
|
|
403
|
+
*
|
|
404
|
+
* @access private
|
|
405
|
+
* @param {ConfigurationOptions} config Config to ingest.
|
|
406
|
+
* @throws {Error|SyntaxError|TypeError} For invalid config values.
|
|
407
|
+
* @returns {object} Immutable config object.
|
|
408
|
+
*/
|
|
409
|
+
export default function ingest(config = {}) {
|
|
410
|
+
const parsed = {
|
|
411
|
+
// I18n configuration
|
|
412
|
+
i18n: validateI18nObject(config.i18n, (i18n) => ({
|
|
413
|
+
dirs: validateI18nDirs(i18n.dirs),
|
|
414
|
+
locales: validateI18nLocales(i18n.locales),
|
|
415
|
+
})),
|
|
416
|
+
|
|
417
|
+
// URL that will prefix all URLs in the browser address bar
|
|
418
|
+
mountUrl: validateMountUrl(config.mountUrl),
|
|
419
|
+
|
|
420
|
+
// Session
|
|
421
|
+
session: validateSessionObject(config.session, (session) => ({
|
|
422
|
+
name: validateSessionName(session.name),
|
|
423
|
+
secret: validateSessionSecret(session.secret),
|
|
424
|
+
secure: validateSessionSecure(session.secure),
|
|
425
|
+
ttl: validateSessionTtl(session.ttl),
|
|
426
|
+
store: validateSessionStore(session.store),
|
|
427
|
+
cookiePath: validateSessionCookiePath(session.cookiePath, '/'),
|
|
428
|
+
cookieSameSite: validateSessionCookieSameSite(session.cookieSameSite, 'Strict'),
|
|
429
|
+
})),
|
|
430
|
+
|
|
431
|
+
// Views configuration
|
|
432
|
+
views: validateViews(config.views),
|
|
433
|
+
|
|
434
|
+
// Pages
|
|
435
|
+
pages: validatePages(config.pages),
|
|
436
|
+
|
|
437
|
+
// Plan
|
|
438
|
+
plan: validatePlan(config.plan),
|
|
439
|
+
|
|
440
|
+
// Hooks
|
|
441
|
+
hooks: validateGlobalHooks(config.hooks),
|
|
442
|
+
|
|
443
|
+
// Plugins
|
|
444
|
+
plugins: validatePlugins(config.plugins),
|
|
445
|
+
|
|
446
|
+
// Events
|
|
447
|
+
events: validateEvents(config.events),
|
|
448
|
+
|
|
449
|
+
// Helmet configuration
|
|
450
|
+
helmetConfigurator: validateHelmetConfigurator(config.helmetConfigurator),
|
|
451
|
+
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
// Freeze to modifications
|
|
455
|
+
Object.freeze(parsed);
|
|
456
|
+
return parsed;
|
|
457
|
+
}
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { MemoryStore } from 'express-session';
|
|
2
|
+
import { resolve } from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import cookieParserFactory from 'cookie-parser';
|
|
5
|
+
import dirname from './dirname.cjs';
|
|
6
|
+
|
|
7
|
+
import configurationIngestor from './configuration-ingestor.js';
|
|
8
|
+
import nunjucks from './nunjucks.js';
|
|
9
|
+
import mountFactory from './mount.js';
|
|
10
|
+
|
|
11
|
+
import staticRoutes from '../routes/static.js';
|
|
12
|
+
import ancillaryRoutes from '../routes/ancillary.js';
|
|
13
|
+
import journeyRoutes from '../routes/journey.js';
|
|
14
|
+
|
|
15
|
+
import preMiddlewareFactory from '../middleware/pre.js';
|
|
16
|
+
import postMiddlewareFactory from '../middleware/post.js';
|
|
17
|
+
|
|
18
|
+
import sessionMiddlewareFactory from '../middleware/session.js';
|
|
19
|
+
import i18nMiddlewareFactory from '../middleware/i18n.js';
|
|
20
|
+
import dataMiddlewareFactory from '../middleware/data.js';
|
|
21
|
+
|
|
22
|
+
import bodyParserMiddlewareFactory from '../middleware/body-parser.js';
|
|
23
|
+
import csrfMiddlewareFactory from '../middleware/csrf.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @access private
|
|
27
|
+
* @typedef {import('../casa').ConfigurationOptions} ConfigurationOptions
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @access private
|
|
32
|
+
* @typedef {import('../casa').ConfigureResult} ConfigureResult
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @access private
|
|
37
|
+
* @typedef {import('../casa').Mounter} Mounter
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Configure some middleware for use in creating a new CASA app.
|
|
42
|
+
*
|
|
43
|
+
* @memberof module:@dwp/govuk-casa
|
|
44
|
+
* @param {ConfigurationOptions} config Configuration options
|
|
45
|
+
* @returns {ConfigureResult} Result
|
|
46
|
+
*/
|
|
47
|
+
export default function configure(config = {}) {
|
|
48
|
+
// Pass the raw config through each plugin's configure phase so they can
|
|
49
|
+
// optionally modify it
|
|
50
|
+
(config.plugins ?? []).forEach((plugin) => {
|
|
51
|
+
plugin.configure(config);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Extract config
|
|
55
|
+
const ingestedConfig = configurationIngestor(config);
|
|
56
|
+
const {
|
|
57
|
+
mountUrl,
|
|
58
|
+
views = [],
|
|
59
|
+
session = {
|
|
60
|
+
secret: 'secret',
|
|
61
|
+
name: 'casasession',
|
|
62
|
+
secure: false,
|
|
63
|
+
ttl: 3600,
|
|
64
|
+
cookieSameSite: true,
|
|
65
|
+
cookiePath: '/',
|
|
66
|
+
store: undefined,
|
|
67
|
+
},
|
|
68
|
+
pages = [],
|
|
69
|
+
plan = null,
|
|
70
|
+
hooks = [],
|
|
71
|
+
plugins = [],
|
|
72
|
+
events = [],
|
|
73
|
+
i18n = {
|
|
74
|
+
dirs: [],
|
|
75
|
+
locales: ['en', 'cy'],
|
|
76
|
+
},
|
|
77
|
+
helmetConfigurator = undefined,
|
|
78
|
+
} = ingestedConfig;
|
|
79
|
+
|
|
80
|
+
// Prepare all page hooks so they are prefixed with the `journey.` scope.
|
|
81
|
+
pages.forEach((page) => {
|
|
82
|
+
/* eslint-disable-next-line no-param-reassign,no-return-assign */
|
|
83
|
+
(page?.hooks ?? []).forEach((h) => h.hook = `journey.${h.hook}`);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Prepare a Nunjucks environment for rendering all templates.
|
|
87
|
+
// Resolve priority: userland templates > CASA templates > GOVUK templates > Plugin templates
|
|
88
|
+
const nunjucksEnv = nunjucks({
|
|
89
|
+
views: [
|
|
90
|
+
...views,
|
|
91
|
+
resolve(dirname, '../../views'),
|
|
92
|
+
resolve(createRequire(dirname).resolve('govuk-frontend'), '../../'),
|
|
93
|
+
],
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Prepare mandatory middleware
|
|
97
|
+
// These _must_ be added to the ExpressJS application at the start and end
|
|
98
|
+
// of all other middleware respectively.
|
|
99
|
+
const preMiddleware = preMiddlewareFactory({ helmetConfigurator });
|
|
100
|
+
const postMiddleware = postMiddlewareFactory();
|
|
101
|
+
|
|
102
|
+
// Prepare common middleware mounted prior to the ancillaryRouter
|
|
103
|
+
const cookieParserMiddleware = cookieParserFactory(session.secret);
|
|
104
|
+
const sessionMiddleware = sessionMiddlewareFactory({
|
|
105
|
+
cookieParserMiddleware,
|
|
106
|
+
secure: session.secure,
|
|
107
|
+
secret: session.secret,
|
|
108
|
+
name: session.name,
|
|
109
|
+
ttl: session.ttl,
|
|
110
|
+
cookieSameSite: session.cookieSameSite,
|
|
111
|
+
cookiePath: session.cookiePath,
|
|
112
|
+
store: session.store ?? new MemoryStore(),
|
|
113
|
+
});
|
|
114
|
+
const i18nMiddleware = i18nMiddlewareFactory({
|
|
115
|
+
directories: [
|
|
116
|
+
// Order is important; latter directories take precedence
|
|
117
|
+
resolve(dirname, '../../locales/'),
|
|
118
|
+
...i18n.dirs,
|
|
119
|
+
],
|
|
120
|
+
languages: i18n.locales,
|
|
121
|
+
});
|
|
122
|
+
const dataMiddleware = dataMiddlewareFactory({
|
|
123
|
+
plan,
|
|
124
|
+
events,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// Prepare form middleware and its constituent parts
|
|
128
|
+
// These are used for any forms, including waypoint page forms.
|
|
129
|
+
const bodyParserMiddleware = bodyParserMiddlewareFactory();
|
|
130
|
+
const csrfMiddleware = csrfMiddlewareFactory();
|
|
131
|
+
|
|
132
|
+
// Setup router to serve up bundled static assets
|
|
133
|
+
const staticRouter = staticRoutes();
|
|
134
|
+
|
|
135
|
+
// Setup ancillary router default stand-alone pages.
|
|
136
|
+
const ancillaryRouter = ancillaryRoutes({
|
|
137
|
+
sessionTtl: session.ttl,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Setup waypoint router, which includes routes for every defined waypoint
|
|
141
|
+
const journeyRouter = journeyRoutes({
|
|
142
|
+
globalHooks: hooks,
|
|
143
|
+
pages,
|
|
144
|
+
plan,
|
|
145
|
+
csrfMiddleware,
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Create the mounting function
|
|
149
|
+
const mount = mountFactory({
|
|
150
|
+
nunjucksEnv,
|
|
151
|
+
mountUrl,
|
|
152
|
+
plan,
|
|
153
|
+
staticRouter,
|
|
154
|
+
ancillaryRouter,
|
|
155
|
+
journeyRouter,
|
|
156
|
+
preMiddleware,
|
|
157
|
+
sessionMiddleware,
|
|
158
|
+
i18nMiddleware,
|
|
159
|
+
bodyParserMiddleware,
|
|
160
|
+
dataMiddleware,
|
|
161
|
+
postMiddleware,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// Prepare configuration result
|
|
165
|
+
const configOutput = {
|
|
166
|
+
// Nunjucks environment, so it can be attached to other ExpressJS instances
|
|
167
|
+
// using `nunjucksEnv.express(myApp); myApp.set('view engine', 'njk');`.
|
|
168
|
+
nunjucksEnv,
|
|
169
|
+
|
|
170
|
+
// Mandatory middleware. User must add these to their ExpressJS app.
|
|
171
|
+
preMiddleware,
|
|
172
|
+
postMiddleware,
|
|
173
|
+
|
|
174
|
+
// Mandatory routers that consumer must mount onto their own ExpressJS parent app
|
|
175
|
+
staticRouter,
|
|
176
|
+
ancillaryRouter,
|
|
177
|
+
journeyRouter,
|
|
178
|
+
|
|
179
|
+
// CSRF middleware. Should be used wherever form pages are built.
|
|
180
|
+
csrfMiddleware,
|
|
181
|
+
|
|
182
|
+
// Other middleware
|
|
183
|
+
// These may be used by the application author to build other custom routes
|
|
184
|
+
cookieParserMiddleware,
|
|
185
|
+
sessionMiddleware,
|
|
186
|
+
bodyParserMiddleware,
|
|
187
|
+
i18nMiddleware,
|
|
188
|
+
dataMiddleware,
|
|
189
|
+
|
|
190
|
+
// Mount function
|
|
191
|
+
mount,
|
|
192
|
+
|
|
193
|
+
// Ingested config
|
|
194
|
+
config: ingestedConfig,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Bootstrap all plugins
|
|
198
|
+
plugins.filter((p) => p.bootstrap).forEach((plugin) => plugin?.bootstrap(configOutput));
|
|
199
|
+
|
|
200
|
+
// Finished configuration
|
|
201
|
+
return configOutput;
|
|
202
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = __dirname;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import logger from './logger.js';
|
|
2
|
+
|
|
3
|
+
const log = logger('lib:end-session');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A convenience for ending the current session, but retaining some data in it,
|
|
7
|
+
* like the current language. It persists an empty session before regenerating
|
|
8
|
+
* a new ID.
|
|
9
|
+
*
|
|
10
|
+
* Note: this will not remove the session from server-side storage, which will
|
|
11
|
+
* instead be left up to the storage mechanism to clean up.
|
|
12
|
+
*
|
|
13
|
+
* @memberof module:@dwp/govuk-casa
|
|
14
|
+
* @param {import('express').Request} req HTTP request
|
|
15
|
+
* @param {Function} next Chain
|
|
16
|
+
* @returns {void}
|
|
17
|
+
*/
|
|
18
|
+
export default function endSession(req, next) {
|
|
19
|
+
const { language } = req.session;
|
|
20
|
+
|
|
21
|
+
Object.entries(req.session).forEach(([k]) => {
|
|
22
|
+
if (!['cookie'].includes(k)) {
|
|
23
|
+
// ESLint disabled as `Object.entries()` returns "own" properties, and
|
|
24
|
+
// all values are being null'd, so not assigned any user-controlled values
|
|
25
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
26
|
+
req.session[k] = null;
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
req.session.save((saveErr) => {
|
|
31
|
+
if (saveErr) {
|
|
32
|
+
log.error(saveErr);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
req.session.regenerate((err) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
log.error(err);
|
|
38
|
+
next(err);
|
|
39
|
+
} else {
|
|
40
|
+
req.session.language = language;
|
|
41
|
+
req.session.save(next);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|