@dwp/govuk-casa 8.0.0-alpha1 → 8.0.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/CHANGELOG.md +22 -0
- package/README.md +1 -1
- package/dist/assets/css/casa-ie8.css +1 -1
- package/dist/assets/css/casa.css +1 -1
- package/dist/casa.d.ts +2 -1
- package/dist/casa.js +3 -1
- package/dist/lib/CasaTemplateLoader.d.ts +11 -3
- package/dist/lib/CasaTemplateLoader.js +38 -2
- package/dist/lib/JourneyContext.d.ts +51 -8
- package/dist/lib/JourneyContext.js +73 -147
- package/dist/lib/MutableRouter.d.ts +19 -19
- package/dist/lib/MutableRouter.js +30 -23
- package/dist/lib/Plan.d.ts +41 -6
- package/dist/lib/Plan.js +84 -17
- package/dist/lib/ValidationError.d.ts +6 -2
- package/dist/lib/ValidationError.js +7 -0
- package/dist/lib/ValidatorFactory.d.ts +72 -13
- package/dist/lib/ValidatorFactory.js +33 -14
- package/dist/lib/configuration-ingestor.d.ts +262 -0
- package/dist/lib/configuration-ingestor.js +464 -0
- package/dist/lib/configure.d.ts +39 -154
- package/dist/lib/configure.js +35 -59
- package/dist/lib/dirname.cjs +1 -1
- package/dist/lib/dirname.d.cts +2 -0
- package/dist/lib/end-session.d.ts +4 -3
- package/dist/lib/end-session.js +30 -8
- package/dist/lib/field.d.ts +53 -55
- package/dist/lib/field.js +96 -54
- package/dist/lib/index.d.ts +14 -0
- package/dist/lib/index.js +54 -0
- package/dist/lib/logger.d.ts +2 -1
- package/dist/lib/logger.js +3 -4
- package/dist/lib/nunjucks-filters.d.ts +11 -11
- package/dist/lib/nunjucks-filters.js +22 -36
- package/dist/lib/nunjucks.d.ts +7 -6
- package/dist/lib/nunjucks.js +6 -6
- package/dist/lib/utils.d.ts +26 -0
- package/dist/lib/utils.js +70 -1
- package/dist/lib/validators/dateObject.js +1 -1
- package/dist/lib/validators/email.js +1 -1
- package/dist/lib/validators/inArray.js +1 -1
- package/dist/lib/validators/index.js +0 -22
- package/dist/lib/validators/postalAddressObject.js +7 -3
- package/dist/lib/validators/required.js +1 -1
- package/dist/lib/waypoint-url.d.ts +13 -7
- package/dist/lib/waypoint-url.js +13 -7
- package/dist/middleware/body-parser.d.ts +2 -1
- package/dist/middleware/body-parser.js +20 -11
- package/dist/middleware/csrf.d.ts +1 -1
- package/dist/middleware/csrf.js +2 -2
- package/dist/middleware/data.d.ts +1 -2
- package/dist/middleware/data.js +19 -15
- package/dist/middleware/dirname.cjs +1 -1
- package/dist/middleware/dirname.d.cts +2 -0
- package/dist/middleware/gather-fields.d.ts +3 -2
- package/dist/middleware/gather-fields.js +14 -7
- package/dist/middleware/i18n.d.ts +1 -1
- package/dist/middleware/i18n.js +16 -11
- package/dist/middleware/post.d.ts +3 -1
- package/dist/middleware/post.js +24 -9
- package/dist/middleware/pre.js +15 -2
- package/dist/middleware/progress-journey.js +15 -17
- package/dist/middleware/sanitise-fields.js +15 -10
- package/dist/middleware/session.d.ts +2 -1
- package/dist/middleware/session.js +65 -52
- package/dist/middleware/skip-waypoint.js +10 -7
- package/dist/middleware/steer-journey.d.ts +3 -2
- package/dist/middleware/steer-journey.js +26 -8
- package/dist/middleware/validate-fields.js +15 -21
- package/dist/mjs/esm-wrapper.js +18 -7
- package/dist/routes/ancillary.d.ts +8 -1
- package/dist/routes/ancillary.js +7 -1
- package/dist/routes/dirname.cjs +1 -1
- package/dist/routes/dirname.d.cts +2 -0
- package/dist/routes/journey.js +20 -24
- package/dist/routes/static.js +10 -9
- package/package.json +41 -22
- package/views/casa/errors/static.njk +11 -0
- package/views/casa/layouts/main.njk +3 -3
package/dist/middleware/pre.js
CHANGED
|
@@ -6,6 +6,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const crypto_1 = require("crypto");
|
|
7
7
|
const helmet_1 = __importDefault(require("helmet"));
|
|
8
8
|
exports.default = () => [
|
|
9
|
+
// Only allow certain request methods
|
|
10
|
+
(req, res, next) => {
|
|
11
|
+
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
12
|
+
const err = new Error(`Unaccepted request method, "${String(req.method).substr(0, 7)}"`);
|
|
13
|
+
err.code = 'unaccepted_request_method';
|
|
14
|
+
next(err);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
next();
|
|
18
|
+
}
|
|
19
|
+
},
|
|
9
20
|
// Prevent caching response in any intermediaries by default, in case it
|
|
10
21
|
// contains sensitive data.
|
|
11
22
|
// The `no-store` setting is to specifically disable the bfcache and prevent
|
|
@@ -20,7 +31,7 @@ exports.default = () => [
|
|
|
20
31
|
// Generate nonces ready for use in Content-Security-Policy header and
|
|
21
32
|
// govuk-frontend template. This same none can be used wherever required.
|
|
22
33
|
(req, res, next) => {
|
|
23
|
-
res.locals.cspNonce = (0, crypto_1.randomBytes)(16).toString(
|
|
34
|
+
res.locals.cspNonce = (0, crypto_1.randomBytes)(16).toString('hex');
|
|
24
35
|
next();
|
|
25
36
|
},
|
|
26
37
|
// Helmet suite of headers
|
|
@@ -29,7 +40,9 @@ exports.default = () => [
|
|
|
29
40
|
contentSecurityPolicy: {
|
|
30
41
|
useDefaults: true,
|
|
31
42
|
directives: {
|
|
32
|
-
|
|
43
|
+
'script-src': ["'self'", 'www.google-analytics.com', 'www.googletagmanager.com', (req, res) => `'nonce-${res.locals.cspNonce}'`],
|
|
44
|
+
'style-src': ["'self'", 'https:', (req, res) => `'nonce-${res.locals.cspNonce}'`],
|
|
45
|
+
'form-action': ["'self'"],
|
|
33
46
|
},
|
|
34
47
|
},
|
|
35
48
|
// // Require referrer to aid navigation
|
|
@@ -14,8 +14,9 @@ const log = (0, logger_js_1.default)('middleware:progress-journey');
|
|
|
14
14
|
const saveAndRedirect = (session, journeyContext, url, res, next) => {
|
|
15
15
|
JourneyContext_js_1.default.putContext(session, journeyContext);
|
|
16
16
|
session.save((err) => {
|
|
17
|
-
if (err)
|
|
17
|
+
if (err) {
|
|
18
18
|
next(err);
|
|
19
|
+
}
|
|
19
20
|
res.redirect(302, url);
|
|
20
21
|
});
|
|
21
22
|
};
|
|
@@ -25,26 +26,23 @@ exports.default = ({ waypoint, plan, mountUrl, }) => [
|
|
|
25
26
|
const traversed = plan.traverse(req.casa.journeyContext);
|
|
26
27
|
const currentIndex = traversed.indexOf(waypoint);
|
|
27
28
|
const nextIndex = Math.max(currentIndex < 0 ? traversed.length - 1 : 0, Math.min(currentIndex + 1, traversed.length - 1));
|
|
28
|
-
const nextWaypoint = traversed[nextIndex];
|
|
29
|
+
const nextWaypoint = traversed[parseInt(nextIndex, 10)];
|
|
29
30
|
log.trace(`currentIndex = ${currentIndex}, nextIndex = ${nextIndex}, currentWaypoint = ${waypoint}, nextWaypoint = ${nextWaypoint}`);
|
|
30
|
-
// TODO: edit mode ---------- !!! considering the dependency graph instead, which will invalidate pages, meaning we can keep sticky mode as the default - the below, redirecting back to origin is fine to leave as is, because the next traversal in steer-journey will put the user on the right waypoint
|
|
31
|
-
// When in edit mode, these are the options for redirecting:
|
|
32
|
-
// - Try sending straight back to editOrigin - if it can't be reached, the "rails" middleware will redirect the user accordingly
|
|
33
|
-
//
|
|
34
|
-
// TODO: if the edit origin is on another sub-app, only that plan will get travsersed, so the user might miss out on forms in between.
|
|
35
|
-
// Or it might be on the same app, but a Plan exists in the middle of this plan, so need to be sure to travsrse that plan fully too.
|
|
36
|
-
// It's looking more likely that we'll need Plans to talk with each other, but that's a headache.
|
|
37
|
-
// Keep it simple.
|
|
38
|
-
// Restrict the functionality. Don't allow this scenario. Devs will need to resort to waya around it. Wait for community to build things and see what solutions emerge for doption into this framework.
|
|
39
31
|
// Edit mode
|
|
40
32
|
// Attempt to take the user back to their original URL. We rely on the
|
|
41
33
|
// `steer-journey` middleware to prevent the user going too far ahead in
|
|
42
|
-
// their permitted journey.
|
|
43
|
-
//
|
|
44
|
-
//
|
|
34
|
+
// their permitted journey. Bear in mind that the `editOrigin` may not be
|
|
35
|
+
// a waypoint at all, but a route path for a custom endpoint, so we can't
|
|
36
|
+
// safely do a traversal check here.
|
|
37
|
+
//
|
|
38
|
+
// The edit mode URL params will be kept on this redirect. This means the
|
|
39
|
+
// user can keep "jumping" to the next _changed_ waypoint, until they get
|
|
40
|
+
// back to the original URL.
|
|
41
|
+
//
|
|
42
|
+
// Devs should use the `events` mechanism to mark waypoints as invalid if
|
|
43
|
+
// they want to force the user to re-visit particular waypoints during this
|
|
44
|
+
// "jumping" phase.
|
|
45
45
|
if (req.casa.editMode && req.casa.editOrigin) {
|
|
46
|
-
// TODO: Need tyo detect if we're already on the editOrigin on this request.
|
|
47
|
-
// If so, need to fall through so we can get directed to the next waypoint in the journey
|
|
48
46
|
const url = new URL(req.casa.editOrigin, 'https://placeholder.test/');
|
|
49
47
|
url.searchParams.append('edit', 'true');
|
|
50
48
|
url.searchParams.append('editorigin', req.casa.editOrigin);
|
|
@@ -77,6 +75,6 @@ exports.default = ({ waypoint, plan, mountUrl, }) => [
|
|
|
77
75
|
});
|
|
78
76
|
// Save and move on
|
|
79
77
|
log.trace(`Redirecting to ${nextUrl}`);
|
|
80
|
-
saveAndRedirect(req.session, req.casa.journeyContext, nextUrl, res, next);
|
|
78
|
+
return saveAndRedirect(req.session, req.casa.journeyContext, nextUrl, res, next);
|
|
81
79
|
},
|
|
82
80
|
];
|
|
@@ -11,38 +11,43 @@ const field_js_1 = __importDefault(require("../lib/field.js"));
|
|
|
11
11
|
const JourneyContext_js_1 = __importDefault(require("../lib/JourneyContext.js"));
|
|
12
12
|
exports.default = ({ waypoint, fields = [], }) => {
|
|
13
13
|
// Add some common, transient fields to ensure they survive beyond this sanitisation process
|
|
14
|
-
fields.push((0, field_js_1.default)('_csrf', { persist: false }).processor((value => String(value)))
|
|
15
|
-
fields.push((0, field_js_1.default)('contextid', { persist: false }).processor((value => String(value)))
|
|
16
|
-
fields.push((0, field_js_1.default)('edit', { persist: false }).processor((value => String(value)))
|
|
17
|
-
fields.push((0, field_js_1.default)('editorigin', { persist: false }).processor((value => String(value)))
|
|
14
|
+
fields.push((0, field_js_1.default)('_csrf', { persist: false }).processor((value) => String(value)));
|
|
15
|
+
fields.push((0, field_js_1.default)('contextid', { persist: false }).processor((value) => String(value)));
|
|
16
|
+
fields.push((0, field_js_1.default)('edit', { persist: false }).processor((value) => String(value)));
|
|
17
|
+
fields.push((0, field_js_1.default)('editorigin', { persist: false }).processor((value) => String(value)));
|
|
18
18
|
// Middleware
|
|
19
19
|
return [
|
|
20
20
|
(req, res, next) => {
|
|
21
21
|
// First, prune all undefined, or unknown fields from `req.body` (i.e.
|
|
22
22
|
// those that do not have an entry in `fields`)
|
|
23
|
+
// EsLint disabled as `fields`, `i` & `name` are only controlled by dev
|
|
24
|
+
/* eslint-disable security/detect-object-injection */
|
|
23
25
|
const prunedBody = Object.create(null);
|
|
24
26
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
25
27
|
if (lodash_1.default.has(req.body, fields[i].name) && req.body[fields[i].name] !== undefined) {
|
|
26
28
|
prunedBody[fields[i].name] = req.body[fields[i].name];
|
|
27
29
|
}
|
|
28
30
|
}
|
|
29
|
-
|
|
31
|
+
/* eslint-enable security/detect-object-injection */
|
|
30
32
|
const journeyContext = JourneyContext_js_1.default.fromContext(req.casa.journeyContext);
|
|
31
33
|
journeyContext.setDataForPage(waypoint, prunedBody);
|
|
32
|
-
// const journeyContext = {};
|
|
33
34
|
// Second, prune any fields that do not pass the validation conditional,
|
|
34
35
|
// and process those that do.
|
|
35
36
|
const sanitisedBody = Object.create(null);
|
|
36
37
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
37
|
-
const field = fields[i];
|
|
38
|
-
|
|
39
|
-
if (fieldValue !== undefined && field.testConditions({
|
|
38
|
+
const field = fields[i]; /* eslint-disable-line security/detect-object-injection */
|
|
39
|
+
const fieldValue = field.getValue(prunedBody);
|
|
40
|
+
if (fieldValue !== undefined && field.testConditions({
|
|
41
|
+
fieldValue,
|
|
42
|
+
waypoint,
|
|
43
|
+
journeyContext,
|
|
44
|
+
})) {
|
|
40
45
|
field.putValue(sanitisedBody, field.applyProcessors(fieldValue));
|
|
41
46
|
}
|
|
42
47
|
}
|
|
43
48
|
// Finally, write the sanitised body back to the request object
|
|
44
49
|
req.body = sanitisedBody;
|
|
45
50
|
next();
|
|
46
|
-
}
|
|
51
|
+
},
|
|
47
52
|
];
|
|
48
53
|
};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export default function
|
|
1
|
+
export default function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl, cookieSameSite, cookiePath, store, }: {
|
|
2
2
|
cookieParserMiddleware: any;
|
|
3
3
|
secret: any;
|
|
4
4
|
name: any;
|
|
@@ -6,5 +6,6 @@ export default function _default({ cookieParserMiddleware, secret, name, secure,
|
|
|
6
6
|
ttl: any;
|
|
7
7
|
mountUrl?: string | undefined;
|
|
8
8
|
cookieSameSite?: boolean | undefined;
|
|
9
|
+
cookiePath?: string | undefined;
|
|
9
10
|
store?: any;
|
|
10
11
|
}): any[];
|
|
@@ -28,14 +28,65 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
28
28
|
const express_session_1 = __importStar(require("express-session"));
|
|
29
29
|
const logger_js_1 = __importDefault(require("../lib/logger.js"));
|
|
30
30
|
const log = (0, logger_js_1.default)('middleware:session');
|
|
31
|
+
const sessionExpiryMiddleware = (mountUrl, ttl, getCookie, touchCookie, removeCookie) => (req, res, next) => {
|
|
32
|
+
var _a;
|
|
33
|
+
const lastModified = getCookie(req);
|
|
34
|
+
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
35
|
+
if (lastModified === 0) {
|
|
36
|
+
// New session, or grace period cookie no longer available after
|
|
37
|
+
// expiring; generate a new session, and create grace-period cookie.
|
|
38
|
+
// This will invalidate any CSRF tokens, so by letting the request POST
|
|
39
|
+
// requests through the user may see a 500 error response.
|
|
40
|
+
log.info('Session is new, or grace period has expired. Regenerating session.');
|
|
41
|
+
req.session.regenerate((err) => {
|
|
42
|
+
if (err) {
|
|
43
|
+
next(err);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
touchCookie(res);
|
|
47
|
+
if (req.method === 'POST') {
|
|
48
|
+
log.info('The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.');
|
|
49
|
+
res.redirect(302, mountUrl);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
next();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else if (age > ttl) {
|
|
58
|
+
// Cookie has become stale and server session will have been removed;
|
|
59
|
+
// redirect to session-timeout
|
|
60
|
+
log.info('Session has timed out within grace period. Destroying session and redirecting to timeout page.');
|
|
61
|
+
const language = (_a = req.session.language) !== null && _a !== void 0 ? _a : 'en';
|
|
62
|
+
req.session.destroy((err) => {
|
|
63
|
+
if (err) {
|
|
64
|
+
next(err);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
removeCookie(res);
|
|
68
|
+
const params = new URLSearchParams({
|
|
69
|
+
referrer: req.originalUrl,
|
|
70
|
+
lang: language,
|
|
71
|
+
});
|
|
72
|
+
res.redirect(302, `${mountUrl}session-timeout?${params.toString()}`);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Touch cookie and continue
|
|
78
|
+
touchCookie(res);
|
|
79
|
+
next();
|
|
80
|
+
}
|
|
81
|
+
};
|
|
31
82
|
// 3 middleware:
|
|
32
83
|
// - set the session cookie
|
|
33
84
|
// - parse request cookies
|
|
34
85
|
// - handle expiry of server-side session
|
|
35
|
-
function
|
|
86
|
+
function sessionMiddleware({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl = '/', cookieSameSite = true, cookiePath = '/', store = new express_session_1.MemoryStore(), }) {
|
|
36
87
|
const commonCookieOptions = {
|
|
37
88
|
httpOnly: true,
|
|
38
|
-
path:
|
|
89
|
+
path: cookiePath,
|
|
39
90
|
secure,
|
|
40
91
|
};
|
|
41
92
|
if (cookieSameSite !== false) {
|
|
@@ -44,8 +95,15 @@ function default_1({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl
|
|
|
44
95
|
const ttlGrace = 1800; // user will see session-timeout if session expires within 30mins
|
|
45
96
|
const touchCookieName = `${name}.t`;
|
|
46
97
|
const touchCookieOptions = Object.assign(Object.assign({}, commonCookieOptions), { maxAge: (ttl + ttlGrace) * 1000, signed: true });
|
|
98
|
+
const getCookie = (req) => {
|
|
99
|
+
var _a;
|
|
100
|
+
// Disabled eslint as `touchCookieName` is a constant, known value
|
|
101
|
+
/* eslint-disable-next-line security/detect-object-injection */
|
|
102
|
+
const lastModified = Date.parse(String((_a = req.signedCookies[touchCookieName]) !== null && _a !== void 0 ? _a : '1970-01-01T00:00:00+0000'));
|
|
103
|
+
return Number.isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001);
|
|
104
|
+
};
|
|
47
105
|
const touchCookie = (res) => {
|
|
48
|
-
// Touch cookie expiry is
|
|
106
|
+
// Touch cookie expiry is a short period after the session ttl. This gives
|
|
49
107
|
// a small period of time where a user will see the session-timeout message,
|
|
50
108
|
// which is important to avoid the confusion of simply being redirected back
|
|
51
109
|
// to the start of their journey.
|
|
@@ -56,60 +114,15 @@ function default_1({ cookieParserMiddleware, secret, name, secure, ttl, mountUrl
|
|
|
56
114
|
};
|
|
57
115
|
return [
|
|
58
116
|
(0, express_session_1.default)({
|
|
59
|
-
secret
|
|
60
|
-
name
|
|
117
|
+
secret,
|
|
118
|
+
name,
|
|
61
119
|
saveUninitialized: false,
|
|
62
120
|
resave: false,
|
|
63
121
|
cookie: Object.assign(Object.assign({}, commonCookieOptions), { maxAge: null }),
|
|
64
122
|
store,
|
|
65
123
|
}),
|
|
66
124
|
cookieParserMiddleware,
|
|
67
|
-
(
|
|
68
|
-
var _a, _b;
|
|
69
|
-
let lastModified = Date.parse(String((_a = req.signedCookies[touchCookieName]) !== null && _a !== void 0 ? _a : '1970-01-01T00:00:00+0000'));
|
|
70
|
-
lastModified = isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001); // convert to seconds
|
|
71
|
-
const age = Math.floor(Date.now() * 0.001) - lastModified;
|
|
72
|
-
if (lastModified === 0) {
|
|
73
|
-
// New session, or grace period cookie no longer available after
|
|
74
|
-
// expiring; generate a new session, and create grace-period cookie.
|
|
75
|
-
// This will invalidate any CSRF tokens, so by letting the request POST
|
|
76
|
-
// requests through the user may see a 500 error response.
|
|
77
|
-
log.info('Session is new, or grace period has expired. Regenerating session.');
|
|
78
|
-
req.session.regenerate((err) => {
|
|
79
|
-
if (err)
|
|
80
|
-
return next(err);
|
|
81
|
-
touchCookie(res);
|
|
82
|
-
if (req.method === 'POST') {
|
|
83
|
-
log.info('The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.');
|
|
84
|
-
res.redirect(302, mountUrl);
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
next();
|
|
88
|
-
}
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
else if (age > ttl) {
|
|
92
|
-
// Cookie has become stale and server session will have been removed;
|
|
93
|
-
// redirect to session-timeout
|
|
94
|
-
log.info('Session has timed out within grace period. Destroying session and redirecting to timeout page.');
|
|
95
|
-
const language = (_b = req.session.language) !== null && _b !== void 0 ? _b : 'en';
|
|
96
|
-
req.session.destroy((err) => {
|
|
97
|
-
if (err)
|
|
98
|
-
return next(err);
|
|
99
|
-
removeCookie(res);
|
|
100
|
-
const params = new URLSearchParams({
|
|
101
|
-
referrer: req.originalUrl,
|
|
102
|
-
lang: language,
|
|
103
|
-
});
|
|
104
|
-
res.redirect(302, `${mountUrl}session-timeout?${params.toString()}`);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
// Touch cookie and continue
|
|
109
|
-
touchCookie(res);
|
|
110
|
-
next();
|
|
111
|
-
}
|
|
112
|
-
},
|
|
125
|
+
sessionExpiryMiddleware(mountUrl, ttl, getCookie, touchCookie, removeCookie),
|
|
113
126
|
];
|
|
114
127
|
}
|
|
115
|
-
exports.default =
|
|
128
|
+
exports.default = sessionMiddleware;
|
|
@@ -4,14 +4,15 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
5
5
|
};
|
|
6
6
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
-
const lodash_1 = require("lodash");
|
|
7
|
+
const lodash_1 = __importDefault(require("lodash"));
|
|
8
8
|
const JourneyContext_js_1 = __importDefault(require("../lib/JourneyContext.js"));
|
|
9
9
|
const waypoint_url_js_1 = __importDefault(require("../lib/waypoint-url.js"));
|
|
10
10
|
const logger_js_1 = __importDefault(require("../lib/logger.js"));
|
|
11
|
+
const { has } = lodash_1.default;
|
|
11
12
|
const log = (0, logger_js_1.default)('middleware:skip-waypoint');
|
|
12
13
|
exports.default = ({ waypoint, mountUrl, }) => [
|
|
13
14
|
(req, res, next) => {
|
|
14
|
-
if (!
|
|
15
|
+
if (!has(req.query, 'skipto')) {
|
|
15
16
|
return next();
|
|
16
17
|
}
|
|
17
18
|
const skipTo = String(req.query.skipto);
|
|
@@ -25,16 +26,18 @@ exports.default = ({ waypoint, mountUrl, }) => [
|
|
|
25
26
|
const redirectUrl = (0, waypoint_url_js_1.default)({
|
|
26
27
|
mountUrl,
|
|
27
28
|
waypoint: skipTo,
|
|
28
|
-
edit: req.editMode,
|
|
29
|
-
editOrigin: req.
|
|
29
|
+
edit: req.casa.editMode,
|
|
30
|
+
editOrigin: req.casa.editOrigin,
|
|
30
31
|
journeyContext: req.casa.journeyContext,
|
|
31
32
|
});
|
|
32
33
|
log.debug(`Will redirect to "${redirectUrl}" after skipping "${waypoint}"`);
|
|
33
|
-
req.session.save((err) => {
|
|
34
|
+
return req.session.save((err) => {
|
|
34
35
|
if (err) {
|
|
35
|
-
|
|
36
|
+
next(err);
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
res.redirect(302, redirectUrl);
|
|
36
40
|
}
|
|
37
|
-
res.redirect(302, redirectUrl);
|
|
38
41
|
});
|
|
39
42
|
},
|
|
40
43
|
];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
declare function _default({ waypoint, plan, mountUrl, }: {
|
|
2
2
|
waypoint: string;
|
|
3
|
-
plan:
|
|
3
|
+
plan: Plan;
|
|
4
4
|
mountUrl: string;
|
|
5
|
-
}):
|
|
5
|
+
}): void;
|
|
6
6
|
export default _default;
|
|
7
|
+
export type Plan = typeof import("../lib/Plan");
|
|
@@ -9,10 +9,17 @@ const waypoint_url_js_1 = __importDefault(require("../lib/waypoint-url.js"));
|
|
|
9
9
|
const logger_js_1 = __importDefault(require("../lib/logger.js"));
|
|
10
10
|
const log = (0, logger_js_1.default)('middleware:steer-journey');
|
|
11
11
|
/**
|
|
12
|
-
* @
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*
|
|
12
|
+
* @typedef {import('../lib/Plan')} Plan
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* This sits in front of all other journey middleware and prevents the user from
|
|
16
|
+
* "jumping ahead" in the Plan.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} obj Options
|
|
19
|
+
* @param {string} obj.waypoint Current waypoint
|
|
20
|
+
* @param {Plan} obj.plan CASA Plan
|
|
21
|
+
* @param {string} obj.mountUrl Mount URL
|
|
22
|
+
* @returns {void}
|
|
16
23
|
*/
|
|
17
24
|
exports.default = ({ waypoint, plan, mountUrl, }) => [
|
|
18
25
|
(req, res, next) => {
|
|
@@ -30,15 +37,26 @@ exports.default = ({ waypoint, plan, mountUrl, }) => [
|
|
|
30
37
|
editOrigin: req.casa.editOrigin,
|
|
31
38
|
}));
|
|
32
39
|
}
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
|
|
40
|
+
// Edit mode
|
|
41
|
+
// Cannot be in edit mode if we're already on the `editorigin` URL
|
|
42
|
+
if (req.casa.editMode) {
|
|
43
|
+
const { pathname: currentPathname } = new URL(req.originalUrl, 'http://placeholder.test/');
|
|
44
|
+
if (req.casa.editOrigin === currentPathname) {
|
|
45
|
+
log.debug(`Disabling edit mode as we are on the edit origin (${req.casa.editOrigin})`);
|
|
46
|
+
req.casa.editMode = false;
|
|
47
|
+
req.casa.editOrigin = undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// difficult: first waypoint on a Plan - how do we determine if there are
|
|
51
|
+
// other plans pointing at this one? and how do we determine if those others
|
|
52
|
+
// are part of a future plan, or a past one? Think we'll have to leave it up
|
|
53
|
+
// to the dev to add the back link for the first page in a Plan.
|
|
36
54
|
// Calculate URL for the "back" link
|
|
37
55
|
const [prevRoute] = plan.traversePrevRoutes(req.casa.journeyContext, {
|
|
38
56
|
startWaypoint: waypoint,
|
|
39
57
|
stopCondition: () => (true), // stop at the first one
|
|
40
58
|
});
|
|
41
59
|
res.locals.casa.journeyPreviousUrl = prevRoute.target ? (0, waypoint_url_js_1.default)({ mountUrl, waypoint: prevRoute.target, routeName: 'prev' }) : undefined;
|
|
42
|
-
next();
|
|
60
|
+
return next();
|
|
43
61
|
},
|
|
44
62
|
];
|
|
@@ -5,7 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
// Validate the data captured in the journey context
|
|
7
7
|
const JourneyContext_js_1 = __importDefault(require("../lib/JourneyContext.js"));
|
|
8
|
-
const updateContext = ({ waypoint, errors = null,
|
|
8
|
+
const updateContext = ({ waypoint, errors = null, journeyContext, session, }) => {
|
|
9
9
|
// Set validation state
|
|
10
10
|
if (errors === null) {
|
|
11
11
|
journeyContext.clearValidationErrorsForPage(waypoint);
|
|
@@ -13,16 +13,6 @@ const updateContext = ({ waypoint, errors = null, plan, mountUrl, journeyContext
|
|
|
13
13
|
else {
|
|
14
14
|
journeyContext.setValidationErrorsForPage(waypoint, errors);
|
|
15
15
|
}
|
|
16
|
-
// // Update nav context
|
|
17
|
-
// // In most cases, the "next" & "prev" routes will result in the same sequence,
|
|
18
|
-
// // in reverse. But devs do have the option of setting different logic on
|
|
19
|
-
// // these routes, so this is the proper way to do it.
|
|
20
|
-
// const traversedNext = plan.traverseNextRoutes(journeyContext).map(r => r.source);
|
|
21
|
-
// const traversedPrev = plan.traversePrevRoutes(journeyContext, {
|
|
22
|
-
// startWaypoint: traversedNext[traversedNext.length - 1],
|
|
23
|
-
// }).map(r => r.source);
|
|
24
|
-
// journeyContext.setNavigationUrls(mountUrl, traversedNext, 'next');
|
|
25
|
-
// journeyContext.setNavigationUrls(mountUrl, traversedPrev, 'prev');
|
|
26
16
|
// Save to session
|
|
27
17
|
JourneyContext_js_1.default.putContext(session, journeyContext);
|
|
28
18
|
};
|
|
@@ -31,6 +21,8 @@ exports.default = ({ waypoint, fields = [], mountUrl, plan, }) => [
|
|
|
31
21
|
var _a, _b;
|
|
32
22
|
let errors = [];
|
|
33
23
|
for (let i = 0, l = fields.length; i < l; i++) {
|
|
24
|
+
/* eslint-disable security/detect-object-injection */
|
|
25
|
+
// Dynamic object keys are only used on known entities (fields, waypoint)
|
|
34
26
|
const field = fields[i];
|
|
35
27
|
const fieldName = field.name;
|
|
36
28
|
const fieldValue = (_b = (_a = req.casa.journeyContext.data) === null || _a === void 0 ? void 0 : _a[waypoint]) === null || _b === void 0 ? void 0 : _b[fieldName];
|
|
@@ -40,12 +32,13 @@ exports.default = ({ waypoint, fields = [], mountUrl, plan, }) => [
|
|
|
40
32
|
waypoint,
|
|
41
33
|
journeyContext: req.casa.journeyContext,
|
|
42
34
|
};
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
35
|
+
/* eslint-enable security/detect-object-injection */
|
|
36
|
+
if (field.testConditions(context)) {
|
|
37
|
+
errors = [
|
|
38
|
+
...errors,
|
|
39
|
+
...field.runValidators(fieldValue, context),
|
|
40
|
+
];
|
|
41
|
+
}
|
|
49
42
|
}
|
|
50
43
|
// Validation passed with no errors
|
|
51
44
|
if (!errors.length) {
|
|
@@ -59,8 +52,9 @@ exports.default = ({ waypoint, fields = [], mountUrl, plan, }) => [
|
|
|
59
52
|
return next();
|
|
60
53
|
}
|
|
61
54
|
// If there are any native errors in the list, we need to bail the request
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
const nativeError = errors.find((e) => e instanceof Error);
|
|
56
|
+
if (nativeError) {
|
|
57
|
+
return next(nativeError);
|
|
64
58
|
}
|
|
65
59
|
// Make the errors available to downstream middleware
|
|
66
60
|
updateContext({
|
|
@@ -71,6 +65,6 @@ exports.default = ({ waypoint, fields = [], mountUrl, plan, }) => [
|
|
|
71
65
|
plan,
|
|
72
66
|
journeyContext: req.casa.journeyContext,
|
|
73
67
|
});
|
|
74
|
-
next();
|
|
75
|
-
}
|
|
68
|
+
return next();
|
|
69
|
+
},
|
|
76
70
|
];
|
package/dist/mjs/esm-wrapper.js
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
// Basic wrapper to act as the package entrypoint for ESM applications
|
|
2
2
|
// ref: https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1
|
|
3
|
+
// The `../casa.js` reference here is correct. This file will be copied into
|
|
4
|
+
// `dist/` in the right location at build time, which will resolve that file
|
|
5
|
+
// path correctly.
|
|
3
6
|
import casa from '../casa.js';
|
|
4
|
-
|
|
5
|
-
export const
|
|
6
|
-
export const
|
|
7
|
-
export const
|
|
8
|
-
export const
|
|
9
|
-
export const
|
|
10
|
-
export const
|
|
7
|
+
|
|
8
|
+
export const { configure } = casa;
|
|
9
|
+
export const { validators } = casa;
|
|
10
|
+
export const { field } = casa;
|
|
11
|
+
export const { Plan } = casa;
|
|
12
|
+
export const { JourneyContext } = casa;
|
|
13
|
+
export const { ValidatorFactory } = casa;
|
|
14
|
+
export const { ValidationError } = casa;
|
|
15
|
+
|
|
16
|
+
// Utilities
|
|
17
|
+
export const { waypointUrl } = casa;
|
|
18
|
+
export const { endSession } = casa;
|
|
19
|
+
|
|
20
|
+
// Nunjucks filters
|
|
21
|
+
export const { nunjucksFilters } = casa;
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an instance of the ancillary router.
|
|
3
|
+
*
|
|
4
|
+
* @param {Object} options = Optiona
|
|
5
|
+
* @param {number} options.sessionTtl Session timeout (seconds)
|
|
6
|
+
* @returns {MutableRouter} Mutable router
|
|
7
|
+
*/
|
|
1
8
|
export default function ancillaryRouter({ sessionTtl, }: {
|
|
2
|
-
sessionTtl:
|
|
9
|
+
sessionTtl: number;
|
|
3
10
|
}): MutableRouter;
|
|
4
11
|
import MutableRouter from "../lib/MutableRouter.js";
|
package/dist/routes/ancillary.js
CHANGED
|
@@ -4,11 +4,17 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const MutableRouter_js_1 = __importDefault(require("../lib/MutableRouter.js"));
|
|
7
|
+
/**
|
|
8
|
+
* Create an instance of the ancillary router.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} options = Optiona
|
|
11
|
+
* @param {number} options.sessionTtl Session timeout (seconds)
|
|
12
|
+
* @returns {MutableRouter} Mutable router
|
|
13
|
+
*/
|
|
7
14
|
function ancillaryRouter({ sessionTtl, }) {
|
|
8
15
|
// Router
|
|
9
16
|
const router = new MutableRouter_js_1.default();
|
|
10
17
|
// Session timeout
|
|
11
|
-
// TODO: add a `ancillary.presessiontimeout` hook here? Might be useful for those who way to enhance the timeout route, rather than replacing it completely
|
|
12
18
|
router.all('/session-timeout', (req, res) => {
|
|
13
19
|
res.render('casa/session-timeout.njk', {
|
|
14
20
|
sessionTtl: Math.floor(sessionTtl / 60),
|
package/dist/routes/dirname.cjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
module.exports = __dirname;
|
|
1
|
+
module.exports = __dirname;
|