@dwp/govuk-casa 8.11.1 → 8.13.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 +1 -1
- package/dist/casa.d.ts +29 -1
- package/dist/casa.js +24 -1
- package/dist/casa.js.map +1 -1
- package/dist/lib/JourneyContext.d.ts +32 -6
- package/dist/lib/JourneyContext.js +70 -11
- package/dist/lib/JourneyContext.js.map +1 -1
- package/dist/lib/configuration-ingestor.d.ts +11 -1
- package/dist/lib/configuration-ingestor.js +61 -1
- package/dist/lib/configuration-ingestor.js.map +1 -1
- package/dist/lib/configure.js +6 -2
- package/dist/lib/configure.js.map +1 -1
- package/dist/lib/constants.d.ts +2 -0
- package/dist/lib/constants.js +3 -1
- package/dist/lib/constants.js.map +1 -1
- package/dist/lib/context-id-generators.d.ts +27 -0
- package/dist/lib/context-id-generators.js +54 -0
- package/dist/lib/context-id-generators.js.map +1 -0
- package/dist/middleware/data.d.ts +2 -1
- package/dist/middleware/data.js +7 -1
- package/dist/middleware/data.js.map +1 -1
- package/dist/middleware/gather-fields.js +1 -1
- package/dist/middleware/gather-fields.js.map +1 -1
- package/dist/middleware/sanitise-fields.js +1 -1
- package/dist/middleware/sanitise-fields.js.map +1 -1
- package/dist/mjs/esm-wrapper.js +1 -0
- package/dist/routes/journey.d.ts +1 -1
- package/dist/routes/journey.js +35 -10
- package/dist/routes/journey.js.map +1 -1
- package/package.json +1 -2
- package/src/casa.js +27 -0
- package/src/lib/JourneyContext.js +79 -11
- package/src/lib/configuration-ingestor.js +42 -0
- package/src/lib/configure.js +7 -0
- package/src/lib/constants.js +2 -0
- package/src/lib/context-id-generators.js +71 -0
- package/src/middleware/data.js +8 -0
- package/src/middleware/gather-fields.js +4 -1
- package/src/middleware/sanitise-fields.js +1 -1
- package/src/routes/journey.js +36 -9
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
/* eslint-disable import/no-cycle */
|
|
1
2
|
/**
|
|
2
3
|
* Represents the state of a user's journey through the Plan. It contains
|
|
3
4
|
* information about:
|
|
@@ -6,11 +7,11 @@
|
|
|
6
7
|
* - Validation errors on that data
|
|
7
8
|
* - Navigation information about how the user got where they are.
|
|
8
9
|
*/
|
|
9
|
-
import { v4 as uuidv4, validate as uuidValidate } from 'uuid';
|
|
10
10
|
import lodash from 'lodash';
|
|
11
11
|
import ValidationError from './ValidationError.js';
|
|
12
12
|
import logger from './logger.js';
|
|
13
13
|
import { notProto } from './utils.js';
|
|
14
|
+
import { uuid as uuidGenerator } from './context-id-generators.js';
|
|
14
15
|
|
|
15
16
|
const {
|
|
16
17
|
cloneDeep, isPlainObject, isObject, has, isEqual,
|
|
@@ -18,6 +19,8 @@ const {
|
|
|
18
19
|
|
|
19
20
|
const log = logger('lib:journey-context');
|
|
20
21
|
|
|
22
|
+
const uuid = uuidGenerator();
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* @access private
|
|
23
26
|
* @typedef {import('../casa').ContextEventUserInfo} ContextEventUserInfo
|
|
@@ -75,6 +78,16 @@ export default class JourneyContext {
|
|
|
75
78
|
|
|
76
79
|
static DEFAULT_CONTEXT_ID = 'default';
|
|
77
80
|
|
|
81
|
+
/**
|
|
82
|
+
* @type {symbol}
|
|
83
|
+
*/
|
|
84
|
+
static ID_GENERATOR_REQ_LOG = Symbol('generatedContextIds');
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* @type {symbol}
|
|
88
|
+
*/
|
|
89
|
+
static ID_GENERATOR_REQ_KEY = Symbol('generateContextId');
|
|
90
|
+
|
|
78
91
|
/**
|
|
79
92
|
* Constructor.
|
|
80
93
|
*
|
|
@@ -476,12 +489,15 @@ export default class JourneyContext {
|
|
|
476
489
|
/**
|
|
477
490
|
* Construct a new ephemeral JourneyContext instance with a unique ID.
|
|
478
491
|
*
|
|
492
|
+
* Note: In later versions of CASA, the `req` property will be mandatory.
|
|
493
|
+
*
|
|
494
|
+
* @param {ExpressRequest} [req] Request session
|
|
479
495
|
* @returns {JourneyContext} Constructed JourneyContext instance
|
|
480
496
|
*/
|
|
481
|
-
static createEphemeralContext() {
|
|
497
|
+
static createEphemeralContext(req) {
|
|
482
498
|
return JourneyContext.fromObject({
|
|
483
499
|
identity: {
|
|
484
|
-
id:
|
|
500
|
+
id: JourneyContext.generateContextId(req),
|
|
485
501
|
},
|
|
486
502
|
});
|
|
487
503
|
}
|
|
@@ -489,17 +505,20 @@ export default class JourneyContext {
|
|
|
489
505
|
/**
|
|
490
506
|
* Construct a new JourneyContext instance from another instance.
|
|
491
507
|
*
|
|
508
|
+
* Note: In later versions of CASA, the `req` property will be mandatory.
|
|
509
|
+
*
|
|
492
510
|
* @param {JourneyContext} context Context to copy from
|
|
511
|
+
* @param {ExpressRequest} [req] Request
|
|
493
512
|
* @returns {JourneyContext} Constructed JourneyContext instance
|
|
494
513
|
* @throws {TypeError} When context is not a valid type
|
|
495
514
|
*/
|
|
496
|
-
static fromContext(context) {
|
|
515
|
+
static fromContext(context, req) {
|
|
497
516
|
if (!(context instanceof JourneyContext)) {
|
|
498
517
|
throw new TypeError('Source context must be a JourneyContext');
|
|
499
518
|
}
|
|
500
519
|
|
|
501
520
|
const newContextObj = context.toObject();
|
|
502
|
-
newContextObj.identity.id =
|
|
521
|
+
newContextObj.identity.id = JourneyContext.generateContextId(req);
|
|
503
522
|
|
|
504
523
|
return JourneyContext.fromObject(newContextObj);
|
|
505
524
|
}
|
|
@@ -542,14 +561,14 @@ export default class JourneyContext {
|
|
|
542
561
|
}
|
|
543
562
|
|
|
544
563
|
/**
|
|
545
|
-
* Validate the format of a context ID
|
|
546
|
-
*
|
|
547
|
-
*
|
|
564
|
+
* Validate the format of a context ID:
|
|
565
|
+
* - Between 1 and 64 characters
|
|
566
|
+
* - Contain only the characters a-z, 0-9, -
|
|
548
567
|
*
|
|
549
568
|
* @param {string} id Context ID
|
|
550
569
|
* @returns {string} Original ID if it's valid
|
|
551
570
|
* @throws {TypeError} When id is not a valid type
|
|
552
|
-
* @throws {SyntaxError} When id is not a valid
|
|
571
|
+
* @throws {SyntaxError} When id is not a valid format
|
|
553
572
|
*/
|
|
554
573
|
static validateContextId(id) {
|
|
555
574
|
if (id === JourneyContext.DEFAULT_CONTEXT_ID) {
|
|
@@ -558,9 +577,58 @@ export default class JourneyContext {
|
|
|
558
577
|
|
|
559
578
|
if (typeof id !== 'string') {
|
|
560
579
|
throw new TypeError('Context ID must be a string');
|
|
561
|
-
} else if (!
|
|
562
|
-
throw new SyntaxError('Context ID is not in the correct
|
|
580
|
+
} else if (!id.match(/^[a-z0-9-]{1,64}$/)) {
|
|
581
|
+
throw new SyntaxError('Context ID is not in the correct format');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return id;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Generate a new context ID, validate it, and throw if the ID has already been
|
|
589
|
+
* generated during this request lifecycle. This may happen if an ID was
|
|
590
|
+
* generated, but never used to store a new context in the session. Therefore
|
|
591
|
+
* it is important for user code to always call `putContext()` before
|
|
592
|
+
* generating another ID.
|
|
593
|
+
*
|
|
594
|
+
* @param {ExpressRequest} [req] Request
|
|
595
|
+
* @returns {string} New ID
|
|
596
|
+
* @throws {Error} When generated ID has already been used
|
|
597
|
+
*/
|
|
598
|
+
static generateContextId(req) {
|
|
599
|
+
// Can't generate custom ID when no request object is provided, because the
|
|
600
|
+
// custom generator function itself exists on that object.
|
|
601
|
+
if (!req) {
|
|
602
|
+
log.warn('Generating a context ID without a given request object. Reverting to uuid().');
|
|
603
|
+
return uuid();
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// Collate a list of context IDs already in use, either from existing
|
|
607
|
+
// contexts in the session, or generated during this request lifecycle.
|
|
608
|
+
// We don't identify the source of each ID because the generator must not
|
|
609
|
+
// differentiate its behaviour on whether the ID exists in session or not.
|
|
610
|
+
const inSessionIds = JourneyContext.getContexts(req.session)
|
|
611
|
+
.map((c) => c.identity.id)
|
|
612
|
+
.filter((id) => id !== JourneyContext.DEFAULT_CONTEXT_ID);
|
|
613
|
+
const inRequestIds = req[JourneyContext.ID_GENERATOR_REQ_LOG] ?? [];
|
|
614
|
+
const reservedIds = Array.from(new Set([...inSessionIds, ...inRequestIds]).values());
|
|
615
|
+
|
|
616
|
+
// Generate and log the ID
|
|
617
|
+
const id = JourneyContext.validateContextId(
|
|
618
|
+
req[JourneyContext.ID_GENERATOR_REQ_KEY].call(null, { req, reservedIds }),
|
|
619
|
+
);
|
|
620
|
+
if (reservedIds.includes(id)) {
|
|
621
|
+
throw new Error(`Regenerated a context ID, ${String(id)}. It has likely not yet been used to store a new context in session using JourneyContext.putContext().`);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
if (!req[JourneyContext.ID_GENERATOR_REQ_LOG]) {
|
|
625
|
+
Object.defineProperty(req, JourneyContext.ID_GENERATOR_REQ_LOG, {
|
|
626
|
+
value: [],
|
|
627
|
+
enumerable: false,
|
|
628
|
+
writable: false,
|
|
629
|
+
});
|
|
563
630
|
}
|
|
631
|
+
req[JourneyContext.ID_GENERATOR_REQ_LOG].push(id);
|
|
564
632
|
|
|
565
633
|
return id;
|
|
566
634
|
}
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
validateHookPath,
|
|
10
10
|
validateView,
|
|
11
11
|
} from './utils.js';
|
|
12
|
+
import * as contextIdGenerators from './context-id-generators.js';
|
|
13
|
+
import { CONFIG_ERROR_VISIBILITY_ALWAYS, CONFIG_ERROR_VISIBILITY_ONSUBMIT } from './constants.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* @access private
|
|
@@ -255,6 +257,25 @@ export function validateSessionCookiePath(cookiePath, defaultPath = '/') {
|
|
|
255
257
|
* @returns {boolean} cookie path
|
|
256
258
|
* @throws {TypeError} When invalid arguments are provided
|
|
257
259
|
*/
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Validates errorVisibility.
|
|
263
|
+
*
|
|
264
|
+
* @access private
|
|
265
|
+
* @param {string} errorVisibility sets visibility flag for page validation error
|
|
266
|
+
* @throws {SyntaxError} For invalid errorVisibility flag.
|
|
267
|
+
* @returns {symbol | Function} flag for error visibility.
|
|
268
|
+
*/
|
|
269
|
+
export function validateErrorVisibility(errorVisibility = CONFIG_ERROR_VISIBILITY_ONSUBMIT) {
|
|
270
|
+
if (errorVisibility === undefined) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
if (errorVisibility === CONFIG_ERROR_VISIBILITY_ALWAYS || errorVisibility === CONFIG_ERROR_VISIBILITY_ONSUBMIT || typeof errorVisibility === 'function') {
|
|
274
|
+
return errorVisibility;
|
|
275
|
+
}
|
|
276
|
+
throw new TypeError('errorVisibility must be casa constant CONFIG_ERROR_VISIBILITY_ALWAYS | CONFIG_ERROR_VISIBILITY_ONSUBMIT or function');
|
|
277
|
+
}
|
|
278
|
+
|
|
258
279
|
export function validateSessionCookieSameSite(cookieSameSite, defaultFlag) {
|
|
259
280
|
const validValues = [true, false, 'Strict', 'Lax', 'None'];
|
|
260
281
|
|
|
@@ -321,6 +342,9 @@ const validatePage = (page, index) => {
|
|
|
321
342
|
if (page.hooks !== undefined) {
|
|
322
343
|
validatePageHooks(page.hooks);
|
|
323
344
|
}
|
|
345
|
+
if (page.errorVisibility !== undefined) {
|
|
346
|
+
validateErrorVisibility(page.errorVisibility)
|
|
347
|
+
}
|
|
324
348
|
} catch (err) {
|
|
325
349
|
err.message = `Page at index ${index} is invalid: ${err.message}`;
|
|
326
350
|
throw err;
|
|
@@ -435,6 +459,18 @@ export function validateFormMaxBytes(value, defaultValue = 1024 * 50) {
|
|
|
435
459
|
return parsedValue;
|
|
436
460
|
}
|
|
437
461
|
|
|
462
|
+
export function validateContextIdGenerator(generator) {
|
|
463
|
+
if (generator === undefined) {
|
|
464
|
+
return contextIdGenerators.uuid();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
if (!(generator instanceof Function)) {
|
|
468
|
+
throw new TypeError('contextIdGenerator must be a function');
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
return generator;
|
|
472
|
+
}
|
|
473
|
+
|
|
438
474
|
/**
|
|
439
475
|
* Ingest, validate, sanitise and manipulate configuration parameters.
|
|
440
476
|
*
|
|
@@ -454,6 +490,9 @@ export default function ingest(config = {}) {
|
|
|
454
490
|
// URL that will prefix all URLs in the browser address bar
|
|
455
491
|
mountUrl: validateMountUrl(config.mountUrl),
|
|
456
492
|
|
|
493
|
+
// flag to make validation error visible on get requests
|
|
494
|
+
errorVisibility: validateErrorVisibility(config.errorVisibility),
|
|
495
|
+
|
|
457
496
|
// Session
|
|
458
497
|
session: validateSessionObject(config.session, (session) => ({
|
|
459
498
|
name: validateSessionName(session.name),
|
|
@@ -489,6 +528,9 @@ export default function ingest(config = {}) {
|
|
|
489
528
|
// Form parsing
|
|
490
529
|
formMaxParams: validateFormMaxParams(config.formMaxParams, 25),
|
|
491
530
|
formMaxBytes: validateFormMaxBytes(config.formMaxBytes, 1024 * 50),
|
|
531
|
+
|
|
532
|
+
// Context ID generator
|
|
533
|
+
contextIdGenerator: validateContextIdGenerator(config.contextIdGenerator),
|
|
492
534
|
};
|
|
493
535
|
|
|
494
536
|
// Freeze to modifications
|
package/src/lib/configure.js
CHANGED
|
@@ -22,6 +22,8 @@ import dataMiddlewareFactory from '../middleware/data.js';
|
|
|
22
22
|
import bodyParserMiddlewareFactory from '../middleware/body-parser.js';
|
|
23
23
|
import csrfMiddlewareFactory from '../middleware/csrf.js';
|
|
24
24
|
|
|
25
|
+
import { CONFIG_ERROR_VISIBILITY_ONSUBMIT } from './constants.js';
|
|
26
|
+
|
|
25
27
|
/**
|
|
26
28
|
* @access private
|
|
27
29
|
* @typedef {import('../casa').ConfigurationOptions} ConfigurationOptions
|
|
@@ -55,6 +57,7 @@ export default function configure(config = {}) {
|
|
|
55
57
|
const ingestedConfig = configurationIngestor(config);
|
|
56
58
|
const {
|
|
57
59
|
mountUrl,
|
|
60
|
+
errorVisibility = CONFIG_ERROR_VISIBILITY_ONSUBMIT,
|
|
58
61
|
views = [],
|
|
59
62
|
session = {
|
|
60
63
|
secret: 'secret',
|
|
@@ -77,6 +80,7 @@ export default function configure(config = {}) {
|
|
|
77
80
|
helmetConfigurator = undefined,
|
|
78
81
|
formMaxParams,
|
|
79
82
|
formMaxBytes,
|
|
83
|
+
contextIdGenerator,
|
|
80
84
|
} = ingestedConfig;
|
|
81
85
|
|
|
82
86
|
// Prepare all page hooks so they are prefixed with the `journey.` scope.
|
|
@@ -124,6 +128,7 @@ export default function configure(config = {}) {
|
|
|
124
128
|
const dataMiddleware = dataMiddlewareFactory({
|
|
125
129
|
plan,
|
|
126
130
|
events,
|
|
131
|
+
contextIdGenerator,
|
|
127
132
|
});
|
|
128
133
|
|
|
129
134
|
// Prepare form middleware and its constituent parts
|
|
@@ -143,11 +148,13 @@ export default function configure(config = {}) {
|
|
|
143
148
|
});
|
|
144
149
|
|
|
145
150
|
// Setup waypoint router, which includes routes for every defined waypoint
|
|
151
|
+
const globalErrorVisibility = errorVisibility
|
|
146
152
|
const journeyRouter = journeyRoutes({
|
|
147
153
|
globalHooks: hooks,
|
|
148
154
|
pages,
|
|
149
155
|
plan,
|
|
150
156
|
csrfMiddleware,
|
|
157
|
+
globalErrorVisibility,
|
|
151
158
|
});
|
|
152
159
|
|
|
153
160
|
// Create the mounting function
|
package/src/lib/constants.js
CHANGED
|
@@ -7,3 +7,5 @@ export const REQUEST_PHASE_GATHER = Symbol('gather');
|
|
|
7
7
|
export const REQUEST_PHASE_VALIDATE = Symbol('validate');
|
|
8
8
|
export const REQUEST_PHASE_REDIRECT = Symbol('redirect');
|
|
9
9
|
export const REQUEST_PHASE_RENDER = Symbol('render');
|
|
10
|
+
export const CONFIG_ERROR_VISIBILITY_ONSUBMIT = Symbol('onsubmit');
|
|
11
|
+
export const CONFIG_ERROR_VISIBILITY_ALWAYS = Symbol('always');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* eslint-disable import/no-cycle */
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @typedef {import('../casa.js').ContextIdGenerator} ContextIdGenerator
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Creates an instance of a UUID generator.
|
|
10
|
+
*
|
|
11
|
+
* @returns {ContextIdGenerator} Generator function
|
|
12
|
+
*/
|
|
13
|
+
const uuid = () => () => randomUUID();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Returns a generator that returns the next incremental integer in a sequence.
|
|
17
|
+
*
|
|
18
|
+
* This generator does not take into account the removal of any contexts from
|
|
19
|
+
* session that were previously assigned a sequential ID. This means that IDs
|
|
20
|
+
* will be re-used when they are freed up.
|
|
21
|
+
*
|
|
22
|
+
* @returns {ContextIdGenerator} Generator function
|
|
23
|
+
*/
|
|
24
|
+
const sequentialInteger = () => ({ reservedIds }) => {
|
|
25
|
+
const contextIds = Array.from(reservedIds).sort();
|
|
26
|
+
|
|
27
|
+
if (!contextIds.length) {
|
|
28
|
+
return '1';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Find the first numeric ID that we can increment
|
|
32
|
+
let lastInSequence;
|
|
33
|
+
do {
|
|
34
|
+
lastInSequence = Number.parseInt(contextIds.pop(), 10);
|
|
35
|
+
} while (contextIds.length && Number.isNaN(lastInSequence));
|
|
36
|
+
|
|
37
|
+
return String(!Number.isNaN(lastInSequence) ? lastInSequence + 1 : 1);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const shortGuid = ({
|
|
41
|
+
length = 5,
|
|
42
|
+
prefix = '',
|
|
43
|
+
pool = 'abcdefhkmnprtwxy346789',
|
|
44
|
+
} = {}) => ({ reservedIds }) => {
|
|
45
|
+
// Ambiguous characters excluded
|
|
46
|
+
const poolSize = pool.length;
|
|
47
|
+
|
|
48
|
+
const maxAttempts = 10;
|
|
49
|
+
let attempts = maxAttempts;
|
|
50
|
+
let id;
|
|
51
|
+
|
|
52
|
+
do {
|
|
53
|
+
id = Array(length).fill(0).map(() => pool.charAt(Math.floor(Math.random() * poolSize))).join('');
|
|
54
|
+
attempts--;
|
|
55
|
+
} while (attempts > 0 && reservedIds.includes(id));
|
|
56
|
+
|
|
57
|
+
if (attempts === 0) {
|
|
58
|
+
throw new Error(`Failed to generate GUID after ${maxAttempts} iterations`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return `${prefix}${id}`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @namespace ContextIdGenerators
|
|
66
|
+
*/
|
|
67
|
+
export {
|
|
68
|
+
uuid,
|
|
69
|
+
sequentialInteger,
|
|
70
|
+
shortGuid,
|
|
71
|
+
};
|
package/src/middleware/data.js
CHANGED
|
@@ -21,6 +21,7 @@ const editOrigin = (req) => {
|
|
|
21
21
|
export default function dataMiddleware({
|
|
22
22
|
plan,
|
|
23
23
|
events,
|
|
24
|
+
contextIdGenerator,
|
|
24
25
|
}) {
|
|
25
26
|
return [
|
|
26
27
|
(req, res, next) => {
|
|
@@ -45,6 +46,13 @@ export default function dataMiddleware({
|
|
|
45
46
|
// Grab chosen language from session
|
|
46
47
|
req.casa.journeyContext.nav.language = req.session.language;
|
|
47
48
|
|
|
49
|
+
// Context ID generator
|
|
50
|
+
Object.defineProperty(req, JourneyContext.ID_GENERATOR_REQ_KEY, {
|
|
51
|
+
value: contextIdGenerator,
|
|
52
|
+
enumerable: false,
|
|
53
|
+
writable: false,
|
|
54
|
+
});
|
|
55
|
+
|
|
48
56
|
/* ------------------------------------------------- Template variables */
|
|
49
57
|
|
|
50
58
|
// Capture mount URL that will be used in generating all browser URLs
|
|
@@ -29,7 +29,10 @@ export default ({
|
|
|
29
29
|
(req, res, next) => {
|
|
30
30
|
// Store a copy of the journey context before modifying it. This is useful
|
|
31
31
|
// for any comparison work that may be done in subsequent middleware.
|
|
32
|
-
req.casa.archivedJourneyContext = JourneyContext.fromContext(
|
|
32
|
+
req.casa.archivedJourneyContext = JourneyContext.fromContext(
|
|
33
|
+
req.casa.journeyContext,
|
|
34
|
+
req,
|
|
35
|
+
);
|
|
33
36
|
|
|
34
37
|
// Ignore data for any non-persistent fields
|
|
35
38
|
// ESLint disabled as `fields`, `i` and `name` are dev-controlled
|
|
@@ -31,7 +31,7 @@ export default ({
|
|
|
31
31
|
}
|
|
32
32
|
/* eslint-enable security/detect-object-injection */
|
|
33
33
|
|
|
34
|
-
const journeyContext = JourneyContext.fromContext(req.casa.journeyContext);
|
|
34
|
+
const journeyContext = JourneyContext.fromContext(req.casa.journeyContext, req);
|
|
35
35
|
journeyContext.setDataForPage(waypoint, prunedBody);
|
|
36
36
|
|
|
37
37
|
// Second, prune any fields that do not pass the validation conditional,
|
package/src/routes/journey.js
CHANGED
|
@@ -9,6 +9,7 @@ import progressJourneyMiddlewareFactory from '../middleware/progress-journey.js'
|
|
|
9
9
|
import waypointUrl from '../lib/waypoint-url.js';
|
|
10
10
|
import logger from '../lib/logger.js';
|
|
11
11
|
import { resolveMiddlewareHooks } from '../lib/utils.js';
|
|
12
|
+
import { CONFIG_ERROR_VISIBILITY_ALWAYS } from '../lib/constants.js';
|
|
12
13
|
|
|
13
14
|
const log = logger('routes:journey');
|
|
14
15
|
|
|
@@ -53,6 +54,26 @@ const renderMiddlewareFactory = (view, contextFactory) => [
|
|
|
53
54
|
},
|
|
54
55
|
];
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* generate page validation error
|
|
59
|
+
*
|
|
60
|
+
* @param {object} errors object of page validation error
|
|
61
|
+
* @param {object} req casa request object
|
|
62
|
+
* @returns {object[]} array of error objects
|
|
63
|
+
*/
|
|
64
|
+
const generateGovukErrors = (errors, req) => Object.values(errors || {}).map(([error]) => ({
|
|
65
|
+
text: req.t(error.summary, error.variables),
|
|
66
|
+
href: error.fieldHref,
|
|
67
|
+
}))
|
|
68
|
+
/**
|
|
69
|
+
* handle errorVisibility flag and function and return boolean
|
|
70
|
+
*
|
|
71
|
+
* @param {symbol | Function} errorVisibility errorVisibility config option
|
|
72
|
+
* @param {object} req casa request object
|
|
73
|
+
* @returns {boolean} true if errorVisibility is "always" or function condition true
|
|
74
|
+
*/
|
|
75
|
+
const resolveErrorVisibility = (req, errorVisibility) => (typeof errorVisibility === 'function' ? errorVisibility({ req }) : errorVisibility === CONFIG_ERROR_VISIBILITY_ALWAYS)
|
|
76
|
+
|
|
56
77
|
/**
|
|
57
78
|
* Create an instance of the router for all waypoints visited during a Journey
|
|
58
79
|
* through the Plan.
|
|
@@ -66,6 +87,7 @@ export default function journeyRouter({
|
|
|
66
87
|
pages,
|
|
67
88
|
plan,
|
|
68
89
|
csrfMiddleware,
|
|
90
|
+
globalErrorVisibility,
|
|
69
91
|
}) {
|
|
70
92
|
// Router
|
|
71
93
|
const router = new MutableRouter();
|
|
@@ -116,7 +138,7 @@ export default function journeyRouter({
|
|
|
116
138
|
];
|
|
117
139
|
|
|
118
140
|
pages.forEach((page) => {
|
|
119
|
-
const { waypoint, view, hooks: pageHooks = [], fields } = page;
|
|
141
|
+
const { waypoint, view, hooks: pageHooks = [], fields, errorVisibility } = page;
|
|
120
142
|
const waypointPath = `/${waypoint}`;
|
|
121
143
|
|
|
122
144
|
let commonWaypointMiddleware = [
|
|
@@ -145,10 +167,18 @@ export default function journeyRouter({
|
|
|
145
167
|
...resolveMiddlewareHooks('journey.poststeer', waypointPath, [...globalHooks, ...pageHooks]),
|
|
146
168
|
|
|
147
169
|
...resolveMiddlewareHooks('journey.prerender', waypointPath, [...globalHooks, ...pageHooks]),
|
|
148
|
-
renderMiddlewareFactory(view, (req) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
170
|
+
renderMiddlewareFactory(view, (req) => {
|
|
171
|
+
const displayErrors = resolveErrorVisibility(req, globalErrorVisibility) || resolveErrorVisibility(req, errorVisibility);
|
|
172
|
+
const errors = displayErrors && (req.casa.journeyContext.getValidationErrorsForPageByField(waypoint) ?? Object.create(null));
|
|
173
|
+
const govukErrors = displayErrors && generateGovukErrors(errors, req);
|
|
174
|
+
|
|
175
|
+
return ({
|
|
176
|
+
formUrl: waypointUrl({ mountUrl: `${req.baseUrl}/`, waypoint }),
|
|
177
|
+
formData: req.casa.journeyContext.getDataForPage(waypoint),
|
|
178
|
+
formErrors: (Object.keys(errors).length && displayErrors) ? errors : null,
|
|
179
|
+
formErrorsGovukArray: (govukErrors.length && displayErrors) ? govukErrors : null,
|
|
180
|
+
})
|
|
181
|
+
}),
|
|
152
182
|
);
|
|
153
183
|
|
|
154
184
|
router.post(
|
|
@@ -193,10 +223,7 @@ export default function journeyRouter({
|
|
|
193
223
|
// first one is shown.
|
|
194
224
|
// Disabling security/detect-object-injection rule because both `errors`
|
|
195
225
|
// and the `k` property are known entities
|
|
196
|
-
const govukErrors =
|
|
197
|
-
text: req.t(errors[k][0].summary, errors[k][0].variables), /* eslint-disable-line security/detect-object-injection */
|
|
198
|
-
href: errors[k][0].fieldHref, /* eslint-disable-line security/detect-object-injection */
|
|
199
|
-
}));
|
|
226
|
+
const govukErrors = generateGovukErrors(errors, req)
|
|
200
227
|
|
|
201
228
|
return {
|
|
202
229
|
formUrl: waypointUrl({ mountUrl: `${req.baseUrl}/`, waypoint }),
|