@dwp/govuk-casa 8.7.12 → 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.
Files changed (168) hide show
  1. package/dist/casa.js +2 -1
  2. package/dist/casa.js.map +1 -0
  3. package/dist/lib/CasaTemplateLoader.js +1 -0
  4. package/dist/lib/CasaTemplateLoader.js.map +1 -0
  5. package/dist/lib/JourneyContext.d.ts +1 -1
  6. package/dist/lib/JourneyContext.js +2 -1
  7. package/dist/lib/JourneyContext.js.map +1 -0
  8. package/dist/lib/MutableRouter.js +1 -0
  9. package/dist/lib/MutableRouter.js.map +1 -0
  10. package/dist/lib/Plan.d.ts +2 -1
  11. package/dist/lib/Plan.js +4 -3
  12. package/dist/lib/Plan.js.map +1 -0
  13. package/dist/lib/ValidationError.js +1 -0
  14. package/dist/lib/ValidationError.js.map +1 -0
  15. package/dist/lib/ValidatorFactory.d.ts +2 -2
  16. package/dist/lib/ValidatorFactory.js +3 -2
  17. package/dist/lib/ValidatorFactory.js.map +1 -0
  18. package/dist/lib/configuration-ingestor.js +1 -0
  19. package/dist/lib/configuration-ingestor.js.map +1 -0
  20. package/dist/lib/configure.js +2 -1
  21. package/dist/lib/configure.js.map +1 -0
  22. package/dist/lib/end-session.js +1 -0
  23. package/dist/lib/end-session.js.map +1 -0
  24. package/dist/lib/field.js +1 -0
  25. package/dist/lib/field.js.map +1 -0
  26. package/dist/lib/index.js +1 -0
  27. package/dist/lib/index.js.map +1 -0
  28. package/dist/lib/logger.js +1 -0
  29. package/dist/lib/logger.js.map +1 -0
  30. package/dist/lib/mount.js +3 -2
  31. package/dist/lib/mount.js.map +1 -0
  32. package/dist/lib/nunjucks-filters.js +1 -0
  33. package/dist/lib/nunjucks-filters.js.map +1 -0
  34. package/dist/lib/nunjucks.js +1 -0
  35. package/dist/lib/nunjucks.js.map +1 -0
  36. package/dist/lib/utils.d.ts +45 -27
  37. package/dist/lib/utils.js +105 -67
  38. package/dist/lib/utils.js.map +1 -0
  39. package/dist/lib/validators/dateObject.js +4 -3
  40. package/dist/lib/validators/dateObject.js.map +1 -0
  41. package/dist/lib/validators/email.js +1 -0
  42. package/dist/lib/validators/email.js.map +1 -0
  43. package/dist/lib/validators/inArray.js +1 -0
  44. package/dist/lib/validators/inArray.js.map +1 -0
  45. package/dist/lib/validators/index.js +1 -0
  46. package/dist/lib/validators/index.js.map +1 -0
  47. package/dist/lib/validators/nino.js +1 -0
  48. package/dist/lib/validators/nino.js.map +1 -0
  49. package/dist/lib/validators/postalAddressObject.d.ts +2 -2
  50. package/dist/lib/validators/postalAddressObject.js +2 -1
  51. package/dist/lib/validators/postalAddressObject.js.map +1 -0
  52. package/dist/lib/validators/regex.js +1 -0
  53. package/dist/lib/validators/regex.js.map +1 -0
  54. package/dist/lib/validators/required.js +1 -0
  55. package/dist/lib/validators/required.js.map +1 -0
  56. package/dist/lib/validators/strlen.js +1 -0
  57. package/dist/lib/validators/strlen.js.map +1 -0
  58. package/dist/lib/validators/wordCount.js +1 -0
  59. package/dist/lib/validators/wordCount.js.map +1 -0
  60. package/dist/lib/waypoint-url.js +1 -0
  61. package/dist/lib/waypoint-url.js.map +1 -0
  62. package/dist/middleware/body-parser.js +1 -0
  63. package/dist/middleware/body-parser.js.map +1 -0
  64. package/dist/middleware/csrf.js +1 -0
  65. package/dist/middleware/csrf.js.map +1 -0
  66. package/dist/middleware/data.js +1 -0
  67. package/dist/middleware/data.js.map +1 -0
  68. package/dist/middleware/gather-fields.js +1 -0
  69. package/dist/middleware/gather-fields.js.map +1 -0
  70. package/dist/middleware/i18n.js +1 -0
  71. package/dist/middleware/i18n.js.map +1 -0
  72. package/dist/middleware/post.js +1 -0
  73. package/dist/middleware/post.js.map +1 -0
  74. package/dist/middleware/pre.js +1 -0
  75. package/dist/middleware/pre.js.map +1 -0
  76. package/dist/middleware/progress-journey.js +1 -0
  77. package/dist/middleware/progress-journey.js.map +1 -0
  78. package/dist/middleware/sanitise-fields.js +1 -0
  79. package/dist/middleware/sanitise-fields.js.map +1 -0
  80. package/dist/middleware/serve-first-waypoint.js +1 -0
  81. package/dist/middleware/serve-first-waypoint.js.map +1 -0
  82. package/dist/middleware/session.js +1 -0
  83. package/dist/middleware/session.js.map +1 -0
  84. package/dist/middleware/skip-waypoint.js +1 -0
  85. package/dist/middleware/skip-waypoint.js.map +1 -0
  86. package/dist/middleware/steer-journey.js +1 -0
  87. package/dist/middleware/steer-journey.js.map +1 -0
  88. package/dist/middleware/strip-proxy-path.js +1 -0
  89. package/dist/middleware/strip-proxy-path.js.map +1 -0
  90. package/dist/middleware/validate-fields.js +1 -0
  91. package/dist/middleware/validate-fields.js.map +1 -0
  92. package/dist/mjs/esm-wrapper.js +10 -15
  93. package/dist/routes/ancillary.js +1 -0
  94. package/dist/routes/ancillary.js.map +1 -0
  95. package/dist/routes/journey.js +1 -0
  96. package/dist/routes/journey.js.map +1 -0
  97. package/dist/routes/static.js +1 -0
  98. package/dist/routes/static.js.map +1 -0
  99. package/locales/cy/error.json +1 -1
  100. package/locales/en/error.json +1 -1
  101. package/package.json +16 -15
  102. package/src/casa.js +320 -0
  103. package/src/lib/CasaTemplateLoader.js +104 -0
  104. package/src/lib/JourneyContext.js +783 -0
  105. package/src/lib/MutableRouter.js +310 -0
  106. package/src/lib/Plan.js +624 -0
  107. package/src/lib/ValidationError.js +163 -0
  108. package/src/lib/ValidatorFactory.js +105 -0
  109. package/src/lib/configuration-ingestor.js +457 -0
  110. package/src/lib/configure.js +202 -0
  111. package/src/lib/dirname.cjs +1 -0
  112. package/src/lib/end-session.js +45 -0
  113. package/src/lib/field.js +456 -0
  114. package/src/lib/index.js +33 -0
  115. package/src/lib/logger.js +16 -0
  116. package/src/lib/mount.js +127 -0
  117. package/src/lib/nunjucks-filters.js +150 -0
  118. package/src/lib/nunjucks.js +53 -0
  119. package/src/lib/utils.js +232 -0
  120. package/src/lib/validators/dateObject.js +169 -0
  121. package/src/lib/validators/email.js +55 -0
  122. package/src/lib/validators/inArray.js +81 -0
  123. package/src/lib/validators/index.js +24 -0
  124. package/src/lib/validators/nino.js +57 -0
  125. package/src/lib/validators/postalAddressObject.js +162 -0
  126. package/src/lib/validators/regex.js +48 -0
  127. package/src/lib/validators/required.js +74 -0
  128. package/src/lib/validators/strlen.js +66 -0
  129. package/src/lib/validators/wordCount.js +70 -0
  130. package/src/lib/waypoint-url.js +93 -0
  131. package/src/middleware/body-parser.js +31 -0
  132. package/src/middleware/csrf.js +29 -0
  133. package/src/middleware/data.js +105 -0
  134. package/src/middleware/dirname.cjs +1 -0
  135. package/src/middleware/gather-fields.js +51 -0
  136. package/src/middleware/i18n.js +106 -0
  137. package/src/middleware/post.js +61 -0
  138. package/src/middleware/pre.js +91 -0
  139. package/src/middleware/progress-journey.js +92 -0
  140. package/src/middleware/sanitise-fields.js +58 -0
  141. package/src/middleware/serve-first-waypoint.js +28 -0
  142. package/src/middleware/session.js +129 -0
  143. package/src/middleware/skip-waypoint.js +46 -0
  144. package/src/middleware/steer-journey.js +78 -0
  145. package/src/middleware/strip-proxy-path.js +56 -0
  146. package/src/middleware/validate-fields.js +84 -0
  147. package/src/routes/ancillary.js +29 -0
  148. package/src/routes/dirname.cjs +1 -0
  149. package/src/routes/journey.js +212 -0
  150. package/src/routes/static.js +77 -0
  151. package/views/casa/components/character-count/README.md +10 -0
  152. package/views/casa/components/character-count/template.njk +6 -2
  153. package/views/casa/components/checkboxes/README.md +43 -34
  154. package/views/casa/components/checkboxes/template.njk +8 -7
  155. package/views/casa/components/date-input/README.md +11 -1
  156. package/views/casa/components/date-input/template.njk +6 -4
  157. package/views/casa/components/input/README.md +9 -0
  158. package/views/casa/components/input/template.njk +6 -2
  159. package/views/casa/components/postal-address-object/README.md +10 -0
  160. package/views/casa/components/postal-address-object/template.njk +20 -5
  161. package/views/casa/components/radios/README.md +49 -24
  162. package/views/casa/components/radios/template.njk +6 -3
  163. package/views/casa/components/select/README.md +65 -0
  164. package/views/casa/components/select/macro.njk +3 -0
  165. package/views/casa/components/select/template.njk +49 -0
  166. package/views/casa/components/textarea/README.md +9 -0
  167. package/views/casa/components/textarea/template.njk +6 -2
  168. package/views/casa/layouts/journey.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
+ }