@dwp/govuk-casa 8.7.12 → 8.9.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 (173) hide show
  1. package/dist/casa.d.ts +12 -1
  2. package/dist/casa.js +10 -2
  3. package/dist/casa.js.map +1 -0
  4. package/dist/lib/CasaTemplateLoader.js +1 -0
  5. package/dist/lib/CasaTemplateLoader.js.map +1 -0
  6. package/dist/lib/JourneyContext.d.ts +12 -3
  7. package/dist/lib/JourneyContext.js +20 -5
  8. package/dist/lib/JourneyContext.js.map +1 -0
  9. package/dist/lib/MutableRouter.js +1 -0
  10. package/dist/lib/MutableRouter.js.map +1 -0
  11. package/dist/lib/Plan.d.ts +1 -1
  12. package/dist/lib/Plan.js +2 -5
  13. package/dist/lib/Plan.js.map +1 -0
  14. package/dist/lib/ValidationError.js +1 -0
  15. package/dist/lib/ValidationError.js.map +1 -0
  16. package/dist/lib/ValidatorFactory.d.ts +2 -2
  17. package/dist/lib/ValidatorFactory.js +3 -2
  18. package/dist/lib/ValidatorFactory.js.map +1 -0
  19. package/dist/lib/configuration-ingestor.js +1 -0
  20. package/dist/lib/configuration-ingestor.js.map +1 -0
  21. package/dist/lib/configure.js +2 -1
  22. package/dist/lib/configure.js.map +1 -0
  23. package/dist/lib/constants.d.ts +9 -0
  24. package/dist/lib/constants.js +13 -0
  25. package/dist/lib/constants.js.map +1 -0
  26. package/dist/lib/end-session.js +1 -0
  27. package/dist/lib/end-session.js.map +1 -0
  28. package/dist/lib/field.js +1 -0
  29. package/dist/lib/field.js.map +1 -0
  30. package/dist/lib/index.js +1 -0
  31. package/dist/lib/index.js.map +1 -0
  32. package/dist/lib/logger.js +1 -0
  33. package/dist/lib/logger.js.map +1 -0
  34. package/dist/lib/mount.js +3 -2
  35. package/dist/lib/mount.js.map +1 -0
  36. package/dist/lib/nunjucks-filters.js +1 -0
  37. package/dist/lib/nunjucks-filters.js.map +1 -0
  38. package/dist/lib/nunjucks.js +1 -0
  39. package/dist/lib/nunjucks.js.map +1 -0
  40. package/dist/lib/utils.d.ts +45 -27
  41. package/dist/lib/utils.js +105 -67
  42. package/dist/lib/utils.js.map +1 -0
  43. package/dist/lib/validators/dateObject.js +4 -3
  44. package/dist/lib/validators/dateObject.js.map +1 -0
  45. package/dist/lib/validators/email.js +1 -0
  46. package/dist/lib/validators/email.js.map +1 -0
  47. package/dist/lib/validators/inArray.js +1 -0
  48. package/dist/lib/validators/inArray.js.map +1 -0
  49. package/dist/lib/validators/index.js +1 -0
  50. package/dist/lib/validators/index.js.map +1 -0
  51. package/dist/lib/validators/nino.js +1 -0
  52. package/dist/lib/validators/nino.js.map +1 -0
  53. package/dist/lib/validators/postalAddressObject.d.ts +2 -2
  54. package/dist/lib/validators/postalAddressObject.js +2 -1
  55. package/dist/lib/validators/postalAddressObject.js.map +1 -0
  56. package/dist/lib/validators/regex.js +1 -0
  57. package/dist/lib/validators/regex.js.map +1 -0
  58. package/dist/lib/validators/required.js +1 -0
  59. package/dist/lib/validators/required.js.map +1 -0
  60. package/dist/lib/validators/strlen.js +1 -0
  61. package/dist/lib/validators/strlen.js.map +1 -0
  62. package/dist/lib/validators/wordCount.js +1 -0
  63. package/dist/lib/validators/wordCount.js.map +1 -0
  64. package/dist/lib/waypoint-url.js +40 -9
  65. package/dist/lib/waypoint-url.js.map +1 -0
  66. package/dist/middleware/body-parser.js +1 -0
  67. package/dist/middleware/body-parser.js.map +1 -0
  68. package/dist/middleware/csrf.js +1 -0
  69. package/dist/middleware/csrf.js.map +1 -0
  70. package/dist/middleware/data.js +1 -0
  71. package/dist/middleware/data.js.map +1 -0
  72. package/dist/middleware/gather-fields.js +10 -2
  73. package/dist/middleware/gather-fields.js.map +1 -0
  74. package/dist/middleware/i18n.js +1 -0
  75. package/dist/middleware/i18n.js.map +1 -0
  76. package/dist/middleware/post.js +1 -0
  77. package/dist/middleware/post.js.map +1 -0
  78. package/dist/middleware/pre.js +1 -0
  79. package/dist/middleware/pre.js.map +1 -0
  80. package/dist/middleware/progress-journey.js +7 -2
  81. package/dist/middleware/progress-journey.js.map +1 -0
  82. package/dist/middleware/sanitise-fields.js +1 -0
  83. package/dist/middleware/sanitise-fields.js.map +1 -0
  84. package/dist/middleware/serve-first-waypoint.js +1 -0
  85. package/dist/middleware/serve-first-waypoint.js.map +1 -0
  86. package/dist/middleware/session.js +1 -0
  87. package/dist/middleware/session.js.map +1 -0
  88. package/dist/middleware/skip-waypoint.js +1 -0
  89. package/dist/middleware/skip-waypoint.js.map +1 -0
  90. package/dist/middleware/steer-journey.js +2 -0
  91. package/dist/middleware/steer-journey.js.map +1 -0
  92. package/dist/middleware/strip-proxy-path.js +1 -0
  93. package/dist/middleware/strip-proxy-path.js.map +1 -0
  94. package/dist/middleware/validate-fields.js +7 -1
  95. package/dist/middleware/validate-fields.js.map +1 -0
  96. package/dist/mjs/esm-wrapper.js +11 -15
  97. package/dist/routes/ancillary.js +1 -0
  98. package/dist/routes/ancillary.js.map +1 -0
  99. package/dist/routes/journey.js +1 -0
  100. package/dist/routes/journey.js.map +1 -0
  101. package/dist/routes/static.js +1 -0
  102. package/dist/routes/static.js.map +1 -0
  103. package/locales/cy/error.json +1 -1
  104. package/locales/en/error.json +1 -1
  105. package/package.json +17 -16
  106. package/src/casa.js +330 -0
  107. package/src/lib/CasaTemplateLoader.js +104 -0
  108. package/src/lib/JourneyContext.js +797 -0
  109. package/src/lib/MutableRouter.js +310 -0
  110. package/src/lib/Plan.js +619 -0
  111. package/src/lib/ValidationError.js +163 -0
  112. package/src/lib/ValidatorFactory.js +105 -0
  113. package/src/lib/configuration-ingestor.js +457 -0
  114. package/src/lib/configure.js +202 -0
  115. package/src/lib/constants.js +9 -0
  116. package/src/lib/dirname.cjs +1 -0
  117. package/src/lib/end-session.js +45 -0
  118. package/src/lib/field.js +456 -0
  119. package/src/lib/index.js +33 -0
  120. package/src/lib/logger.js +16 -0
  121. package/src/lib/mount.js +127 -0
  122. package/src/lib/nunjucks-filters.js +150 -0
  123. package/src/lib/nunjucks.js +53 -0
  124. package/src/lib/utils.js +232 -0
  125. package/src/lib/validators/dateObject.js +169 -0
  126. package/src/lib/validators/email.js +55 -0
  127. package/src/lib/validators/inArray.js +81 -0
  128. package/src/lib/validators/index.js +24 -0
  129. package/src/lib/validators/nino.js +57 -0
  130. package/src/lib/validators/postalAddressObject.js +162 -0
  131. package/src/lib/validators/regex.js +48 -0
  132. package/src/lib/validators/required.js +74 -0
  133. package/src/lib/validators/strlen.js +66 -0
  134. package/src/lib/validators/wordCount.js +70 -0
  135. package/src/lib/waypoint-url.js +126 -0
  136. package/src/middleware/body-parser.js +31 -0
  137. package/src/middleware/csrf.js +29 -0
  138. package/src/middleware/data.js +105 -0
  139. package/src/middleware/dirname.cjs +1 -0
  140. package/src/middleware/gather-fields.js +58 -0
  141. package/src/middleware/i18n.js +106 -0
  142. package/src/middleware/post.js +61 -0
  143. package/src/middleware/pre.js +91 -0
  144. package/src/middleware/progress-journey.js +96 -0
  145. package/src/middleware/sanitise-fields.js +58 -0
  146. package/src/middleware/serve-first-waypoint.js +28 -0
  147. package/src/middleware/session.js +129 -0
  148. package/src/middleware/skip-waypoint.js +46 -0
  149. package/src/middleware/steer-journey.js +79 -0
  150. package/src/middleware/strip-proxy-path.js +56 -0
  151. package/src/middleware/validate-fields.js +89 -0
  152. package/src/routes/ancillary.js +29 -0
  153. package/src/routes/dirname.cjs +1 -0
  154. package/src/routes/journey.js +212 -0
  155. package/src/routes/static.js +77 -0
  156. package/views/casa/components/character-count/README.md +10 -0
  157. package/views/casa/components/character-count/template.njk +6 -2
  158. package/views/casa/components/checkboxes/README.md +43 -34
  159. package/views/casa/components/checkboxes/template.njk +8 -7
  160. package/views/casa/components/date-input/README.md +11 -1
  161. package/views/casa/components/date-input/template.njk +6 -4
  162. package/views/casa/components/input/README.md +9 -0
  163. package/views/casa/components/input/template.njk +6 -2
  164. package/views/casa/components/postal-address-object/README.md +10 -0
  165. package/views/casa/components/postal-address-object/template.njk +20 -5
  166. package/views/casa/components/radios/README.md +49 -24
  167. package/views/casa/components/radios/template.njk +6 -3
  168. package/views/casa/components/select/README.md +65 -0
  169. package/views/casa/components/select/macro.njk +3 -0
  170. package/views/casa/components/select/template.njk +49 -0
  171. package/views/casa/components/textarea/README.md +9 -0
  172. package/views/casa/components/textarea/template.njk +6 -2
  173. package/views/casa/layouts/journey.njk +1 -1
@@ -0,0 +1,96 @@
1
+ // Determine where to take the user next
2
+ // We assume that the waypoint has been validated prior to reaching this
3
+ // middleware.
4
+
5
+ import Plan from '../lib/Plan.js';
6
+ import JourneyContext from '../lib/JourneyContext.js';
7
+ import waypointUrl from '../lib/waypoint-url.js';
8
+ import logger from '../lib/logger.js';
9
+ import { REQUEST_PHASE_REDIRECT } from '../lib/constants.js';
10
+
11
+ const log = logger('middleware:progress-journey');
12
+
13
+ const saveAndRedirect = (session, journeyContext, url, res, next) => {
14
+ JourneyContext.putContext(session, journeyContext, {
15
+ userInfo: {
16
+ casaRequestPhase: REQUEST_PHASE_REDIRECT,
17
+ },
18
+ });
19
+
20
+ session.save((err) => {
21
+ if (err) {
22
+ next(err);
23
+ }
24
+ res.redirect(302, url);
25
+ });
26
+ };
27
+
28
+ export default ({
29
+ waypoint,
30
+ plan,
31
+ }) => [
32
+ (req, res, next) => {
33
+ // Determine the next available waypoint after the current one
34
+ const traversed = plan.traverse(req.casa.journeyContext);
35
+ const currentIndex = traversed.indexOf(waypoint);
36
+ const nextIndex = Math.max(
37
+ currentIndex < 0 ? traversed.length - 1 : 0,
38
+ Math.min(currentIndex + 1, traversed.length - 1),
39
+ );
40
+ const nextWaypoint = traversed[parseInt(nextIndex, 10)];
41
+ log.trace(`currentIndex = ${currentIndex}, nextIndex = ${nextIndex}, currentWaypoint = ${waypoint}, nextWaypoint = ${nextWaypoint}`);
42
+
43
+ // Edit mode
44
+ // Attempt to take the user back to their original URL. We rely on the
45
+ // `steer-journey` middleware to prevent the user going too far ahead in
46
+ // their permitted journey. Bear in mind that the `editOrigin` may not be
47
+ // a waypoint at all, but a route path for a custom endpoint, so we can't
48
+ // safely do a traversal check here.
49
+ //
50
+ // The edit mode URL params will be kept on this redirect. This means the
51
+ // user can keep "jumping" to the next _changed_ waypoint, until they get
52
+ // back to the original URL.
53
+ //
54
+ // Devs should use the `events` mechanism to mark waypoints as invalid if
55
+ // they want to force the user to re-visit particular waypoints during this
56
+ // "jumping" phase.
57
+ if (req.casa.editMode && req.casa.editOrigin) {
58
+ const url = new URL(req.casa.editOrigin, 'https://placeholder.test/');
59
+ url.searchParams.append('edit', 'true');
60
+ url.searchParams.append('editorigin', req.casa.editOrigin);
61
+ const redirectUrl = waypointUrl({ waypoint: url.pathname }) + url.search.toString();
62
+
63
+ log.debug(`Edit mode detected; redirecting to ${redirectUrl}`);
64
+
65
+ return saveAndRedirect(req.session, req.casa.journeyContext, redirectUrl, res, next);
66
+ }
67
+
68
+ // If the next URL is an "exit node", we need to flag that node as
69
+ // being validated so that subsequent traversals of this journey continue
70
+ // correctly to any waypoints leading on from this one.
71
+ // This effectively says that the other Plan linked to by the exit node is
72
+ // complete, but of course that may not be the case.
73
+ // It would be prudent for developers to add a conditions to the route to
74
+ // check is this is the case, eg
75
+ // setRoute('a', 'b');
76
+ // setRoute('b', 'url:///otherapp/')
77
+ // setRoute('url:////otherapp/', 'c', (r, c) => checkIfOtherAppIsFinished())
78
+ if (Plan.isExitNode(nextWaypoint)) {
79
+ log.trace(`Next waypoint is an exit node; clearing validation state on ${nextWaypoint}`);
80
+ req.casa.journeyContext.clearValidationErrorsForPage(nextWaypoint);
81
+ }
82
+
83
+ // Construct the next url
84
+ const nextUrl = waypointUrl({
85
+ waypoint: nextWaypoint,
86
+ mountUrl: `${req.baseUrl}/`,
87
+ journeyContext: req.casa.journeyContext,
88
+ edit: req.casa.editMode,
89
+ editOrigin: req.casa.editOrigin,
90
+ });
91
+
92
+ // Save and move on
93
+ log.trace(`Redirecting to ${nextUrl}`);
94
+ return saveAndRedirect(req.session, req.casa.journeyContext, nextUrl, res, next);
95
+ },
96
+ ];
@@ -0,0 +1,58 @@
1
+ // Sanitise the fields submitted from a form
2
+ // - Coerce each field to its correct type
3
+ // - Remove an extraneous fields that are not know to the application
4
+
5
+ import _ from 'lodash';
6
+ import fieldFactory from '../lib/field.js';
7
+ import JourneyContext from '../lib/JourneyContext.js';
8
+
9
+ export default ({
10
+ waypoint,
11
+ fields = [],
12
+ }) => {
13
+ // Add some common, transient fields to ensure they survive beyond this sanitisation process
14
+ fields.push(fieldFactory('_csrf', { persist: false }).processor((value) => String(value)));
15
+ fields.push(fieldFactory('contextid', { persist: false }).processor((value) => String(value)));
16
+ fields.push(fieldFactory('edit', { persist: false }).processor((value) => String(value)));
17
+ fields.push(fieldFactory('editorigin', { persist: false }).processor((value) => String(value)));
18
+
19
+ // Middleware
20
+ return [
21
+ (req, res, next) => {
22
+ // First, prune all undefined, or unknown fields from `req.body` (i.e.
23
+ // those that do not have an entry in `fields`)
24
+ // EsLint disabled as `fields`, `i` & `name` are only controlled by dev
25
+ /* eslint-disable security/detect-object-injection */
26
+ const prunedBody = Object.create(null);
27
+ for (let i = 0, l = fields.length; i < l; i++) {
28
+ if (_.has(req.body, fields[i].name) && req.body[fields[i].name] !== undefined) {
29
+ prunedBody[fields[i].name] = req.body[fields[i].name];
30
+ }
31
+ }
32
+ /* eslint-enable security/detect-object-injection */
33
+
34
+ const journeyContext = JourneyContext.fromContext(req.casa.journeyContext);
35
+ journeyContext.setDataForPage(waypoint, prunedBody);
36
+
37
+ // Second, prune any fields that do not pass the validation conditional,
38
+ // and process those that do.
39
+ const sanitisedBody = Object.create(null);
40
+ for (let i = 0, l = fields.length; i < l; i++) {
41
+ const field = fields[i]; /* eslint-disable-line security/detect-object-injection */
42
+ const fieldValue = field.getValue(prunedBody);
43
+
44
+ if (fieldValue !== undefined && field.testConditions({
45
+ fieldValue,
46
+ waypoint,
47
+ journeyContext,
48
+ })) {
49
+ field.putValue(sanitisedBody, field.applyProcessors(fieldValue));
50
+ }
51
+ }
52
+
53
+ // Finally, write the sanitised body back to the request object
54
+ req.body = sanitisedBody;
55
+ next();
56
+ },
57
+ ];
58
+ }
@@ -0,0 +1,28 @@
1
+ import { validateUrlPath } from '../lib/utils.js';
2
+
3
+ /**
4
+ * @access private
5
+ * @typedef {import('express').RequestHandler} ExpressRequestHandler
6
+ */
7
+
8
+ /**
9
+ * @access private
10
+ * @typedef {import('../casa').Plan} Plan
11
+ */
12
+
13
+ /**
14
+ * Redirect the user to the first Plan waypoint when they request the root /
15
+ * path.
16
+ *
17
+ * @param {Plan} plan CASA Plan
18
+ * @returns {ExpressRequestHandler[]} Array of middleware
19
+ */
20
+ export default ({
21
+ plan,
22
+ }) => [(req, res) => {
23
+ const reqUrl = new URL(req.url, 'https://placeholder.test/');
24
+ const reqPath = validateUrlPath(`${req.baseUrl}${reqUrl.pathname}${plan.getWaypoints()[0]}`);
25
+ let reqParams = reqUrl.searchParams.toString();
26
+ reqParams = reqParams ? `?${reqParams}` : '';
27
+ res.redirect(302, `${reqPath}${reqParams}`);
28
+ }];
@@ -0,0 +1,129 @@
1
+ // A last-modified cookie is used to control whether the end user sees a
2
+ // session-timeout page, or they are simply given a new session without
3
+ // interrupting their journey.
4
+ import expressSession, { MemoryStore } from 'express-session';
5
+ import logger from '../lib/logger.js';
6
+ import { validateUrlPath } from '../lib/utils.js';
7
+
8
+ const log = logger('middleware:session');
9
+
10
+ const sessionExpiryMiddleware = (
11
+ ttl,
12
+ getCookie,
13
+ touchCookie,
14
+ removeCookie,
15
+ ) => (req, res, next) => {
16
+ const lastModified = getCookie(req);
17
+ const age = Math.floor(Date.now() * 0.001) - lastModified;
18
+
19
+ if (lastModified === 0) {
20
+ // New session, or grace period cookie no longer available after
21
+ // expiring; generate a new session, and create grace-period cookie.
22
+ // This will invalidate any CSRF tokens, so by letting the request POST
23
+ // requests through the user may see a 500 error response.
24
+ log.info('Session is new, or grace period has expired. Regenerating session.');
25
+ req.session.regenerate((err) => {
26
+ if (err) {
27
+ next(err);
28
+ } else {
29
+ touchCookie(res);
30
+ if (req.method === 'POST') {
31
+ log.info('The CSRF token for this POST request will now be invalid for this regenerated session. Redirecting to app mount point.');
32
+ res.redirect(302, validateUrlPath(`${req.baseUrl}/`));
33
+ } else {
34
+ next();
35
+ }
36
+ }
37
+ });
38
+ } else if (age > ttl) {
39
+ // Cookie has become stale and server session will have been removed;
40
+ // redirect to session-timeout
41
+ log.info('Session has timed out within grace period. Destroying session and redirecting to timeout page.');
42
+ const language = req.session.language ?? 'en';
43
+ req.session.destroy((err) => {
44
+ if (err) {
45
+ next(err);
46
+ } else {
47
+ removeCookie(res);
48
+ const params = new URLSearchParams({
49
+ referrer: req.originalUrl,
50
+ lang: language,
51
+ });
52
+ /* eslint-disable-next-line prefer-template */
53
+ res.redirect(302, validateUrlPath(`${req.baseUrl}/session-timeout`) + `?${params.toString()}`);
54
+ }
55
+ });
56
+ } else {
57
+ // Touch cookie and continue
58
+ touchCookie(res);
59
+ next();
60
+ }
61
+ };
62
+
63
+ // 3 middleware:
64
+ // - set the session cookie
65
+ // - parse request cookies
66
+ // - handle expiry of server-side session
67
+ export default function sessionMiddleware({
68
+ cookieParserMiddleware,
69
+ secret,
70
+ name,
71
+ secure,
72
+ ttl,
73
+ cookieSameSite = true,
74
+ cookiePath = '/',
75
+ store = new MemoryStore(),
76
+ }) {
77
+ const commonCookieOptions = {
78
+ httpOnly: true,
79
+ path: cookiePath,
80
+ secure,
81
+ };
82
+
83
+ if (cookieSameSite !== false) {
84
+ commonCookieOptions.sameSite = cookieSameSite === true ? 'Strict' : cookieSameSite;
85
+ }
86
+
87
+ const ttlGrace = 1800; // user will see session-timeout if session expires within 30mins
88
+ const touchCookieName = `${name}.t`;
89
+ const touchCookieOptions = {
90
+ ...commonCookieOptions,
91
+ maxAge: (ttl + ttlGrace) * 1000,
92
+ signed: true,
93
+ };
94
+
95
+ const getCookie = (req) => {
96
+ // Disabled eslint as `touchCookieName` is a constant, known value
97
+ /* eslint-disable-next-line security/detect-object-injection */
98
+ const lastModified = Date.parse(String(req.signedCookies[touchCookieName] ?? '1970-01-01T00:00:00+0000'));
99
+ return Number.isNaN(lastModified) ? 0 : Math.floor(lastModified * 0.001);
100
+ }
101
+
102
+ const touchCookie = (res) => {
103
+ // Touch cookie expiry is a short period after the session ttl. This gives
104
+ // a small period of time where a user will see the session-timeout message,
105
+ // which is important to avoid the confusion of simply being redirected back
106
+ // to the start of their journey.
107
+ res.cookie(touchCookieName, (new Date(Date.now())).toUTCString(), touchCookieOptions);
108
+ }
109
+
110
+ const removeCookie = (res) => {
111
+ res.clearCookie(touchCookieName, touchCookieOptions);
112
+ }
113
+
114
+ return [
115
+ expressSession({
116
+ secret,
117
+ name,
118
+ saveUninitialized: false,
119
+ resave: false,
120
+ cookie: {
121
+ ...commonCookieOptions,
122
+ maxAge: null,
123
+ },
124
+ store,
125
+ }),
126
+ cookieParserMiddleware,
127
+ sessionExpiryMiddleware(ttl, getCookie, touchCookie, removeCookie),
128
+ ];
129
+ }
@@ -0,0 +1,46 @@
1
+ // Mark a waypoint as skipped
2
+
3
+ import lodash from 'lodash';
4
+ import JourneyContext from '../lib/JourneyContext.js';
5
+ import waypointUrl from '../lib/waypoint-url.js';
6
+ import logger from '../lib/logger.js';
7
+
8
+ const { has } = lodash;
9
+
10
+ const log = logger('middleware:skip-waypoint');
11
+
12
+ export default ({
13
+ waypoint,
14
+ }) => [
15
+ (req, res, next) => {
16
+ if (!has(req.query, 'skipto')) {
17
+ return next();
18
+ }
19
+ const skipTo = String(req.query.skipto);
20
+
21
+ // Inject a special `__skipped__` attribute into this waypoint's data
22
+ log.info(`Marking waypoint "${waypoint}" as skipped`);
23
+ req.casa.journeyContext.clearValidationErrorsForPage(waypoint);
24
+ req.casa.journeyContext.setDataForPage(waypoint, {
25
+ __skipped__: true,
26
+ });
27
+ JourneyContext.putContext(req.session, req.casa.journeyContext);
28
+
29
+ const redirectUrl = waypointUrl({
30
+ mountUrl: `${req.baseUrl}/`,
31
+ waypoint: skipTo,
32
+ edit: req.casa.editMode,
33
+ editOrigin: req.casa.editOrigin,
34
+ journeyContext: req.casa.journeyContext,
35
+ });
36
+ log.debug(`Will redirect to "${redirectUrl}" after skipping "${waypoint}"`);
37
+
38
+ return req.session.save((err) => {
39
+ if (err) {
40
+ next(err);
41
+ } else {
42
+ res.redirect(302, redirectUrl);
43
+ }
44
+ });
45
+ },
46
+ ];
@@ -0,0 +1,79 @@
1
+ // This sits in front of all other middleware and prevents the user from
2
+ // "jumping ahead" in the Plan.
3
+
4
+ import waypointUrl from '../lib/waypoint-url.js';
5
+ import logger from '../lib/logger.js';
6
+
7
+ const log = logger('middleware:steer-journey');
8
+
9
+ /**
10
+ * @access private
11
+ * @typedef {import('../lib/Plan')} Plan
12
+ */
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
+ * @returns {void}
22
+ */
23
+ export default ({
24
+ waypoint,
25
+ plan,
26
+ }) => [
27
+ (req, res, next) => {
28
+ const mountUrl = `${req.baseUrl}/`;
29
+
30
+ // If the requested waypoint doesn't exist in the traversed journey, send
31
+ // the user back to the last good waypoint.
32
+ const traversed = plan.traverse(req.casa.journeyContext);
33
+ if (traversed.indexOf(waypoint) === -1) {
34
+ const redirectTo = traversed[traversed.length - 1];
35
+ log.trace(`Attempted to access "${waypoint}" when not in the journey; redirecting to "${redirectTo}"`);
36
+
37
+ return res.redirect(302, waypointUrl({
38
+ waypoint: redirectTo,
39
+ mountUrl,
40
+ journeyContext: req.casa.journeyContext,
41
+ edit: req.casa.editMode,
42
+ editOrigin: req.casa.editOrigin,
43
+ }));
44
+ }
45
+
46
+ // Edit mode
47
+ // Cannot be in edit mode if we're already on the `editorigin` URL
48
+ if (req.casa.editMode) {
49
+ const { pathname: currentPathname } = new URL(req.originalUrl, 'https://placeholder.test/');
50
+
51
+ if (req.casa.editOrigin === currentPathname) {
52
+ log.debug(`Disabling edit mode as we are on the edit origin (${req.casa.editOrigin})`);
53
+ req.casa.editMode = false;
54
+ req.casa.editOrigin = undefined;
55
+ }
56
+ }
57
+
58
+ // difficult: first waypoint on a Plan - how do we determine if there are
59
+ // other plans pointing at this one? and how do we determine if those others
60
+ // are part of a future plan, or a past one? Think we'll have to leave it up
61
+ // to the dev to add the back link for the first page in a Plan.
62
+
63
+ // Calculate URL for the "back" link
64
+ const [prevRoute] = plan.traversePrevRoutes(req.casa.journeyContext, {
65
+ startWaypoint: waypoint,
66
+ stopCondition: () => (true), // stop at the first one
67
+ });
68
+ res.locals.casa.journeyPreviousUrl = prevRoute.target ? waypointUrl({
69
+ mountUrl,
70
+ journeyContext: req.casa.journeyContext,
71
+ waypoint: prevRoute.target,
72
+ routeName: 'prev',
73
+ edit: req.casa.editMode,
74
+ editOrigin: req.casa.editOrigin,
75
+ }) : undefined;
76
+
77
+ return next();
78
+ },
79
+ ];
@@ -0,0 +1,56 @@
1
+ // Strip any "proxy path" prefix present on the request URL
2
+ //
3
+ // The default "mountUrl" will be match whatever path that the CASA app
4
+ // instance will be mounted onto. So if you mount on `/a/b/c` then all
5
+ // URLs generated for the browser will be prefixed with `/a/b/c`.
6
+ //
7
+ // Defining a `mountUrl` will change that behaviour into a "proxy mode".
8
+ // This mode assumes you have a forwarding proxy that will alter
9
+ // the incoming request paths from `mountUrl` to the original path you
10
+ // have mounted this CASA app instance onto.
11
+ //
12
+ // For example, if you mount the app on `/a/b/c/x`, but want the browser
13
+ // URLs to just use the `/x` prefix, then pass a `mountUrl` of `/x`.
14
+ //
15
+ // See docs in `docs/guides/setup-behind-a-proxy.md`
16
+
17
+ import logger from '../lib/logger.js';
18
+
19
+ const log = logger('casa:middleware:strip-proxy-path');
20
+
21
+ export default ({
22
+ mountUrl = '/',
23
+ }) => [
24
+ (req, res, next) => {
25
+ // TODO:
26
+ // We _may_ have to start tracking the various prefix in order to differentiate
27
+ // between a proxy prefix, and a parent app's path.
28
+
29
+ // Assume everything before `mountUrl` is the proxy path prefix and remove it
30
+ req.originalBaseUrl = req.originalBaseUrl ?? req.baseUrl;
31
+ req.baseUrl = mountUrl.replace(/\/$/, '');
32
+
33
+ // If the app has been mounted directly on the specific `mountUrl`, then
34
+ // there's nothing we need to do and can let this request pass-through.
35
+ if (req.baseUrl === req.originalBaseUrl) {
36
+ next();
37
+ } else if (req.__CASA_BASE_URL_REWRITTEN__) {
38
+ delete req.__CASA_BASE_URL_REWRITTEN__;
39
+ next();
40
+ } else {
41
+ // Strip the proxy path prefix from the original URL so that
42
+ // subsequent middleware sees the URL path as though proxy wasn't there.
43
+ // req.url will already have the proxy prefix and mountUrl removed.
44
+ /* eslint-disable security/detect-non-literal-regexp */
45
+ log.trace(`req.originalUrl before proxy stripping: ${req.originalUrl}`);
46
+ req.originalUrl = req.originalUrl.replace(new RegExp(`^/.+?${mountUrl}`), mountUrl);
47
+ log.trace(`req.originalUrl after proxy stripping: ${req.originalUrl}`);
48
+ /* eslint-enable security/detect-non-literal-regexp */
49
+
50
+ // Issuing this call will re-run this same middleware, so we use this
51
+ // `__CASA_BASE_URL_REWRITTEN__` flag to prevent recursion.
52
+ req.__CASA_BASE_URL_REWRITTEN__ = true;
53
+ req.app.handle(req, res, next);
54
+ }
55
+ },
56
+ ];
@@ -0,0 +1,89 @@
1
+ // Validate the data captured in the journey context
2
+ import JourneyContext from '../lib/JourneyContext.js';
3
+ import { REQUEST_PHASE_VALIDATE } from '../lib/constants.js';
4
+
5
+ const updateContext = ({
6
+ waypoint,
7
+ errors = null,
8
+ journeyContext,
9
+ session,
10
+ }) => {
11
+ // Set validation state
12
+ if (errors === null) {
13
+ journeyContext.clearValidationErrorsForPage(waypoint);
14
+ } else {
15
+ journeyContext.setValidationErrorsForPage(waypoint, errors);
16
+ }
17
+
18
+ // Save to session
19
+ JourneyContext.putContext(session, journeyContext, {
20
+ userInfo: {
21
+ casaRequestPhase: REQUEST_PHASE_VALIDATE,
22
+ },
23
+ });
24
+ }
25
+
26
+ export default ({
27
+ waypoint,
28
+ fields = [],
29
+ plan,
30
+ }) => [
31
+ (req, res, next) => {
32
+ const mountUrl = `${req.baseUrl}/`;
33
+
34
+ // Run validators for every field to build up a complete list of errors
35
+ // currently associated with this waypoint.
36
+ let errors = [];
37
+ for (let i = 0, l = fields.length; i < l; i++) {
38
+ /* eslint-disable security/detect-object-injection */
39
+ // Dynamic object keys are only used on known entities (fields, waypoint)
40
+ const field = fields[i];
41
+ const fieldName = field.name;
42
+ const fieldValue = req.casa.journeyContext.data?.[waypoint]?.[fieldName];
43
+
44
+ // (type = ValidateContext)
45
+ const context = {
46
+ fieldName,
47
+ fieldValue,
48
+ waypoint,
49
+ journeyContext: req.casa.journeyContext,
50
+ };
51
+ /* eslint-enable security/detect-object-injection */
52
+
53
+ errors = [
54
+ ...errors,
55
+ ...field.runValidators(fieldValue, context),
56
+ ];
57
+ }
58
+
59
+ // Validation passed with no errors
60
+ if (!errors.length) {
61
+ updateContext({
62
+ waypoint,
63
+ session: req.session,
64
+ mountUrl,
65
+ plan,
66
+ journeyContext: req.casa.journeyContext,
67
+ });
68
+ return next();
69
+ }
70
+
71
+ // If there are any native errors in the list, we need to bail the request
72
+ const nativeError = errors.find((e) => e instanceof Error);
73
+ if (nativeError) {
74
+ return next(nativeError);
75
+ }
76
+
77
+ // Make the errors available to downstream middleware
78
+ updateContext({
79
+ errors,
80
+ waypoint,
81
+ session: req.session,
82
+ mountUrl,
83
+ plan,
84
+ journeyContext: req.casa.journeyContext,
85
+ });
86
+
87
+ return next();
88
+ },
89
+ ];
@@ -0,0 +1,29 @@
1
+ import MutableRouter from '../lib/MutableRouter.js';
2
+
3
+ /**
4
+ * @typedef {object} AncillaryRouterOptions Options to configure static router
5
+ * @property {number} sessionTtl Session timeout (seconds)
6
+ */
7
+
8
+ /**
9
+ * Create an instance of the ancillary router.
10
+ *
11
+ * @access private
12
+ * @param {AncillaryRouterOptions} options Options
13
+ * @returns {MutableRouter} ExpressJS Router instance
14
+ */
15
+ export default function ancillaryRouter({
16
+ sessionTtl,
17
+ }) {
18
+ // Router
19
+ const router = new MutableRouter();
20
+
21
+ // Session timeout
22
+ router.all('/session-timeout', (req, res) => {
23
+ res.render('casa/session-timeout.njk', {
24
+ sessionTtl: Math.floor(sessionTtl / 60),
25
+ });
26
+ });
27
+
28
+ return router;
29
+ }
@@ -0,0 +1 @@
1
+ module.exports = __dirname;