hof 20.0.0-beta.2 → 20.0.0-beta.20

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 (59) hide show
  1. package/.github/workflows/automate-publish.yml +1 -1
  2. package/.github/workflows/automate-tag.yml +1 -1
  3. package/README.md +329 -256
  4. package/build/lib/mkdir.js +2 -2
  5. package/components/date/index.js +37 -26
  6. package/components/date/templates/date.html +3 -3
  7. package/components/emailer/index.js +49 -41
  8. package/components/emailer/transports/debug.js +1 -2
  9. package/components/index.js +2 -1
  10. package/components/notify/index.js +60 -0
  11. package/components/notify/notify.js +25 -0
  12. package/components/summary/index.js +18 -0
  13. package/config/hof-defaults.js +5 -3
  14. package/config/rate-limits.js +20 -0
  15. package/config/sanitisation-rules.js +29 -0
  16. package/controller/base-controller.js +26 -8
  17. package/controller/controller.js +11 -15
  18. package/frontend/govuk-template/build/config.js +1 -1
  19. package/frontend/template-mixins/mixins/template-mixins.js +12 -9
  20. package/frontend/template-mixins/partials/forms/checkbox-group.html +12 -3
  21. package/frontend/template-mixins/partials/forms/input-text-date.html +1 -1
  22. package/frontend/template-mixins/partials/forms/input-text-group.html +3 -3
  23. package/frontend/template-mixins/partials/forms/option-group.html +12 -3
  24. package/frontend/template-mixins/partials/forms/select.html +3 -3
  25. package/frontend/template-mixins/partials/forms/textarea-group.html +3 -3
  26. package/frontend/template-mixins/partials/mixins/panel.html +1 -2
  27. package/frontend/template-partials/translations/src/en/errors.json +12 -0
  28. package/frontend/template-partials/views/partials/form.html +2 -1
  29. package/frontend/template-partials/views/rate-limit-error.html +10 -0
  30. package/frontend/themes/gov-uk/client-js/govuk-cookies.js +43 -44
  31. package/frontend/themes/gov-uk/client-js/index.js +2 -2
  32. package/frontend/themes/gov-uk/client-js/skip-to-main.js +18 -17
  33. package/frontend/themes/gov-uk/styles/govuk.scss +4 -0
  34. package/frontend/themes/gov-uk/styles/modules/_validation.scss +2 -2
  35. package/frontend/toolkit/assets/javascript/form-focus.js +10 -1
  36. package/frontend/toolkit/assets/javascript/validation.js +6 -1
  37. package/index.js +9 -4
  38. package/lib/router.js +2 -1
  39. package/lib/settings.js +9 -8
  40. package/middleware/errors.js +32 -0
  41. package/middleware/index.js +2 -1
  42. package/middleware/rate-limiter.js +98 -0
  43. package/package.json +6 -6
  44. package/sandbox/apps/sandbox/fields.js +11 -12
  45. package/sandbox/apps/sandbox/index.js +1 -5
  46. package/sandbox/assets/scss/app.scss +0 -52
  47. package/sandbox/package.json +2 -0
  48. package/sandbox/public/css/app.css +4908 -4965
  49. package/sandbox/public/js/bundle.js +79 -65
  50. package/sandbox/server.js +5 -0
  51. package/sandbox/yarn.lock +25 -1
  52. package/transpiler/lib/write-files.js +1 -2
  53. package/utilities/helpers/index.js +16 -1
  54. package/wizard/index.js +1 -0
  55. package/.nyc_output/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  56. package/.nyc_output/processinfo/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  57. package/.nyc_output/processinfo/index.json +0 -1
  58. package/.vscode/settings.json +0 -6
  59. package/sandbox/.env +0 -1
@@ -1,6 +1,8 @@
1
1
  /* eslint-disable no-unused-vars */
2
2
  'use strict';
3
3
 
4
+ const rateLimitsConfig = require('../config/rate-limits');
5
+
4
6
  const errorTitle = code => `${code}_ERROR`;
5
7
  const errorMsg = code => `There is a ${code}_ERROR`;
6
8
  // eslint-disable-next-line complexity
@@ -21,6 +23,36 @@ const getContent = (err, translate) => {
21
23
  content.message = (translate && translate('errors.cookies-required.message'));
22
24
  }
23
25
 
26
+ if (err.code === 'DDOS_RATE_LIMIT') {
27
+ err.status = 429;
28
+ err.template = 'rate-limit-error';
29
+ err.title = (translate && translate('errors.ddos-rate-limit.title'));
30
+ err.message = (translate && translate('errors.ddos-rate-limit.message'));
31
+ err.preTimeToWait = (translate && translate('errors.ddos-rate-limit.pre-time-to-wait'));
32
+ err.timeToWait = rateLimitsConfig.rateLimits.requests.windowSizeInMinutes;
33
+ err.postTimeToWait = (translate && translate('errors.ddos-rate-limit.post-time-to-wait'));
34
+ content.title = (translate && translate('errors.ddos-rate-limit.title'));
35
+ content.message = (translate && translate('errors.ddos-rate-limit.message'));
36
+ content.preTimeToWait = (translate && translate('errors.ddos-rate-limit.pre-time-to-wait'));
37
+ content.timeToWait = rateLimitsConfig.rateLimits.requests.windowSizeInMinutes;
38
+ content.postTimeToWait = (translate && translate('errors.ddos-rate-limit.post-time-to-wait'));
39
+ }
40
+
41
+ if (err.code === 'SUBMISSION_RATE_LIMIT') {
42
+ err.status = 429;
43
+ err.template = 'rate-limit-error';
44
+ err.title = (translate && translate('errors.submission-rate-limit.title'));
45
+ err.message = (translate && translate('errors.submission-rate-limit.message'));
46
+ err.preTimeToWait = (translate && translate('errors.submission-rate-limit.pre-time-to-wait'));
47
+ err.timeToWait = rateLimitsConfig.rateLimits.submissions.windowSizeInMinutes;
48
+ err.postTimeToWait = (translate && translate('errors.submission-rate-limit.post-time-to-wait'));
49
+ content.title = (translate && translate('errors.submission-rate-limit.title'));
50
+ content.message = (translate && translate('errors.submission-rate-limit.message'));
51
+ content.preTimeToWait = (translate && translate('errors.submission-rate-limit.pre-time-to-wait'));
52
+ content.timeToWait = rateLimitsConfig.rateLimits.submissions.windowSizeInMinutes;
53
+ content.postTimeToWait = (translate && translate('errors.submission-rate-limit.post-time-to-wait'));
54
+ }
55
+
24
56
  err.code = err.code || 'UNKNOWN';
25
57
  err.status = err.status || 500;
26
58
 
@@ -4,5 +4,6 @@ module.exports = {
4
4
  cookies: require('./cookies'),
5
5
  errors: require('./errors'),
6
6
  notFound: require('./not-found'),
7
- deepTranslate: require('./deep-translate')
7
+ deepTranslate: require('./deep-translate'),
8
+ rateLimiter: require('./rate-limiter')
8
9
  };
@@ -0,0 +1,98 @@
1
+
2
+ const moment = require('moment');
3
+ const redis = require('redis');
4
+ const config = require('./../config/hof-defaults');
5
+
6
+ module.exports = (options, rateLimitType) => {
7
+ // eslint-disable-next-line no-console
8
+ const logger = options.logger || { log: (func, msg) => console[func](msg) };
9
+ const rateLimits = options.rateLimits[rateLimitType];
10
+ const timestampName = `${rateLimitType}TimeStamp`;
11
+ const countName = `${rateLimitType}Count`;
12
+
13
+ const WINDOW_SIZE_IN_MINUTES = rateLimits.windowSizeInMinutes;
14
+ const MAX_WINDOW_REQUEST_COUNT = rateLimits.maxWindowRequestCount;
15
+ const WINDOW_LOG_INTERVAL_IN_MINUTES = rateLimits.windowLogIntervalInMinutes;
16
+ const ERROR_CODE = rateLimits.errCode;
17
+
18
+ return async (req, res, next) => {
19
+ const redisClient = redis.createClient(config.redis);
20
+
21
+ // check that redis client exists
22
+ if (!redisClient) {
23
+ logger.log('error', 'Redis client does not exist!');
24
+ return next();
25
+ }
26
+
27
+ const closeConnection = async err => {
28
+ await redisClient.quit();
29
+ return next(err);
30
+ };
31
+
32
+ try {
33
+ // fetch records of current user using IP address, returns null when no record is found
34
+ return await redisClient.get(req.ip, async (err, record) => {
35
+ if (err) {
36
+ logger.log('error', `Error with requesting redis session for rate limiting: ${err}`);
37
+ return await closeConnection();
38
+ }
39
+ const currentRequestTime = moment();
40
+ const windowStartTimestamp = moment().subtract(WINDOW_SIZE_IN_MINUTES, 'minutes').unix();
41
+ let oldRecord = false;
42
+ let data;
43
+ // if no record is found , create a new record for user and store to redis
44
+ if (record) {
45
+ data = JSON.parse(record);
46
+ oldRecord = data[data.length - 1][timestampName] < windowStartTimestamp;
47
+ }
48
+
49
+ if (!record || oldRecord) {
50
+ const newRecord = [];
51
+ const requestLog = {
52
+ [timestampName]: currentRequestTime.unix(),
53
+ [countName]: 1
54
+ };
55
+ newRecord.push(requestLog);
56
+ await redisClient.set(req.ip, JSON.stringify(newRecord));
57
+ return await closeConnection();
58
+ }
59
+ // if record is found, parse it's value and calculate number of requests users has made within the last window
60
+ const requestsWithinWindow = data.filter(entry => entry[timestampName] > windowStartTimestamp);
61
+
62
+ const totalWindowRequestsCount = requestsWithinWindow.reduce((accumulator, entry) => {
63
+ return accumulator + entry[countName];
64
+ }, 0);
65
+
66
+ if (!options.rateLimits.env || options.rateLimits.env === 'development') {
67
+ const requestsRemaining = MAX_WINDOW_REQUEST_COUNT - totalWindowRequestsCount;
68
+ const msg = `Requests made by client: ${totalWindowRequestsCount}\nRequests remaining: ${requestsRemaining}`;
69
+ logger.log('info', msg);
70
+ }
71
+ // if number of requests made is greater than or equal to the desired maximum, return error
72
+ if (totalWindowRequestsCount >= MAX_WINDOW_REQUEST_COUNT) {
73
+ return await closeConnection({ code: ERROR_CODE });
74
+ }
75
+ // if number of requests made is less than allowed maximum, log new entry
76
+ const lastRequestLog = data[data.length - 1];
77
+ const potentialCurrentWindowIntervalStartTimeStamp = currentRequestTime
78
+ .subtract(WINDOW_LOG_INTERVAL_IN_MINUTES, 'minutes')
79
+ .unix();
80
+ // if interval has not passed since last request log, increment counter
81
+ if (lastRequestLog[timestampName] > potentialCurrentWindowIntervalStartTimeStamp) {
82
+ lastRequestLog[countName]++;
83
+ data[data.length - 1] = lastRequestLog;
84
+ } else {
85
+ // if interval has passed, log new entry for current user and timestamp
86
+ data.push({
87
+ [timestampName]: currentRequestTime.unix(),
88
+ [countName]: 1
89
+ });
90
+ }
91
+ await redisClient.set(req.ip, JSON.stringify(data));
92
+ return await closeConnection();
93
+ });
94
+ } catch (err) {
95
+ return await closeConnection(err);
96
+ }
97
+ };
98
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hof",
3
3
  "description": "A bootstrap for HOF projects",
4
- "version": "20.0.0-beta.2",
4
+ "version": "20.0.0-beta.20",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
7
7
  "author": "HomeOffice",
@@ -70,16 +70,16 @@
70
70
  "lodash": "^4.17.21",
71
71
  "markdown-it": "^12.3.2",
72
72
  "minimatch": "^3.0.3",
73
- "minimist": "^1.2.0",
73
+ "minimist": "^1.2.6",
74
74
  "mixwith": "^0.1.1",
75
- "mkdirp": "^0.5.1",
76
- "moment": "^2.24.0",
75
+ "moment": "^2.29.2",
77
76
  "morgan": "^1.10.0",
78
77
  "mustache": "^2.3.0",
79
78
  "nodemailer": "^6.6.3",
80
79
  "nodemailer-ses-transport": "^1.5.0",
81
80
  "nodemailer-smtp-transport": "^2.7.4",
82
81
  "nodemailer-stub-transport": "^1.1.0",
82
+ "notifications-node-client": "^5.1.1",
83
83
  "redis": "^3.1.2",
84
84
  "reqres": "^3.0.1",
85
85
  "request": "^2.79.0",
@@ -88,8 +88,8 @@
88
88
  "serve-static": "^1.14.1",
89
89
  "uglify-js": "^3.14.3",
90
90
  "underscore": "^1.12.1",
91
- "urijs": "^1.19.10",
92
- "winston": "^3.3.3"
91
+ "urijs": "^1.19.11",
92
+ "winston": "^3.7.2"
93
93
  },
94
94
  "devDependencies": {
95
95
  "@cucumber/cucumber": "^7.3.0",
@@ -8,7 +8,7 @@ module.exports = {
8
8
  'landing-page-radio': {
9
9
  mixin: 'radio-group',
10
10
  validate: ['required'],
11
- noHeading: true,
11
+ isPageHeading: true,
12
12
  // Design system says to avoid in-line unless it's two options,
13
13
  // so just added as an example below but by default it isn't
14
14
  className: ['govuk-radios--inline'],
@@ -16,12 +16,12 @@ module.exports = {
16
16
  },
17
17
  name: {
18
18
  validate: ['required', 'notUrl', { type: 'maxlength', arguments: 200 }],
19
+ // need to remove this for the heading to go
19
20
  labelClassName: ['govuk-label--l'],
20
- // noHeading: 'true'
21
+ isPageHeading: 'true'
21
22
  },
22
23
  'dateOfBirth': dateComponent('dateOfBirth', {
23
- // noHeading: 'true',
24
- controlType: 'date-input',
24
+ isPageHeading: 'true',
25
25
  validate: [
26
26
  'required',
27
27
  'date',
@@ -29,28 +29,24 @@ module.exports = {
29
29
  ]
30
30
  }),
31
31
  building: {
32
- noHeading: 'true',
33
32
  validate: ['required', 'notUrl', { type: 'maxlength', arguments: 100 }]
34
33
  },
35
34
  street: {
36
- noHeading: 'true',
37
35
  validate: ['notUrl', { type: 'maxlength', arguments: 50 }],
38
36
  labelClassName: 'visuallyhidden'
39
37
  },
40
38
  townOrCity: {
41
- noHeading: 'true',
42
39
  validate: ['required', 'notUrl',
43
40
  { type: 'regex', arguments: /^([^0-9]*)$/ },
44
41
  { type: 'maxlength', arguments: 100 }
45
42
  ]
46
43
  },
47
44
  postcode: {
48
- noHeading: 'true',
49
45
  validate: ['required', 'postcode'],
50
46
  formatter: ['removespaces', 'uppercase']
51
47
  },
52
48
  incomeTypes: {
53
- // noHeading: 'true',
49
+ isPageHeading: 'true',
54
50
  mixin: 'checkbox-group',
55
51
  labelClassName: 'visuallyhidden',
56
52
  validate: ['required'],
@@ -63,7 +59,7 @@ module.exports = {
63
59
  ]
64
60
  },
65
61
  countryOfHearing: {
66
- // noHeading: 'true',
62
+ isPageHeading: 'true',
67
63
  mixin: 'radio-group',
68
64
  validate: ['required'],
69
65
  options: [
@@ -73,7 +69,7 @@ module.exports = {
73
69
  ]
74
70
  },
75
71
  email: {
76
- // noHeading: 'true',
72
+ isPageHeading: 'true',
77
73
  labelClassName: ['govuk-label--l'],
78
74
  validate: ['required', 'email']
79
75
  },
@@ -86,6 +82,7 @@ module.exports = {
86
82
  },
87
83
  countrySelect: {
88
84
  mixin: 'select',
85
+ isPageHeading: 'true',
89
86
  className: ['typeahead'],
90
87
  options:[''].concat(require('homeoffice-countries').allCountries),
91
88
  legend: {
@@ -97,12 +94,13 @@ module.exports = {
97
94
  mixin: 'textarea',
98
95
  // we want to ignore default formatters as we want
99
96
  // to preserve white space
97
+ isPageHeading: 'true',
100
98
  'ignore-defaults': true,
101
99
  // apply the other default formatters
102
100
  formatter: ['trim', 'hyphens'],
103
101
  labelClassName: ['govuk-label--l'],
104
102
  // attributes here are passed to the field element
105
- validate: ['required', { type: 'maxlength', arguments: 100 }],
103
+ validate: ['required', { type: 'maxlength', arguments: 10 }],
106
104
  attributes: [{
107
105
  attribute: 'rows',
108
106
  value: 8
@@ -110,6 +108,7 @@ module.exports = {
110
108
  },
111
109
  appealStages: {
112
110
  mixin: 'select',
111
+ isPageHeading: 'true',
113
112
  validate: ['required'],
114
113
  options: [{
115
114
  value: '',
@@ -30,11 +30,7 @@ module.exports = {
30
30
  },
31
31
  '/dob': {
32
32
  fields: ['dateOfBirth'],
33
- next: '/address',
34
- locals: {
35
- step: 'dob',
36
- labelClassName: 'govuk-input'
37
- }
33
+ next: '/address'
38
34
  },
39
35
  '/address': {
40
36
  fields: ['building', 'street', 'townOrCity', 'postcode'],
@@ -1,6 +1,5 @@
1
1
 
2
2
  @import "../../../frontend/themes/gov-uk/styles/govuk";
3
- @import "../../../node_modules/govuk-frontend/govuk/all";
4
3
 
5
4
  //autocomplete styling
6
5
  .tt-menu {
@@ -26,54 +25,3 @@
26
25
  .twitter-typeahead {
27
26
  width: 100%;
28
27
  }
29
-
30
- // govuk cookie banner styling
31
- .govuk-banner--success {
32
- border-color: #00703c;
33
- color: #00703c;
34
- }
35
-
36
- .govuk-banner {
37
- border: 5px solid #1d70b8;
38
- font-size: 0;
39
- margin-bottom: 30px;
40
- padding: 10px;
41
- }
42
-
43
- .govuk-banner__icon{
44
- display: inline-block;
45
- }
46
-
47
- .govuk-banner__message {
48
- font-family: "GDS Transport", Arial, sans-serif;
49
- -webkit-font-smoothing: antialiased;
50
- font-weight: 400;
51
- font-size: 1rem;
52
- line-height: 1.25;
53
- color: #0b0c0c;
54
- display: block;
55
- overflow: hidden;
56
- display: inline-block;
57
- margin-left: 10px;
58
- }
59
-
60
- .govuk-banner__assistive {
61
- position: absolute !important;
62
- width: 1px !important;
63
- height: 1px !important;
64
- margin: 0 !important;
65
- padding: 0 !important;
66
- overflow: hidden !important;
67
- clip: rect(0 0 0 0) !important;
68
- clip-path: inset(50%) !important;
69
- border: 0 !important;
70
- white-space: nowrap !important;
71
- }
72
-
73
- .cookie-table-holder > table > tbody > tr > td:first-child{
74
- font-weight:bold;
75
- }
76
-
77
- .js-enabled #global-cookie-message {
78
- display: none;
79
- }
@@ -15,7 +15,9 @@
15
15
  },
16
16
  "author": "",
17
17
  "dependencies": {
18
+ "govuk-frontend": "3.14",
18
19
  "jquery": "^3.6.0",
20
+ "sass": "^1.53.0",
19
21
  "typeahead-aria": "^1.0.4"
20
22
  },
21
23
  "devDependencies": {