hof 20.0.0-beta.1 → 20.0.0-beta.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. package/.github/workflows/automate-publish.yml +1 -1
  2. package/.github/workflows/automate-tag.yml +1 -1
  3. package/.nyc_output/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +1 -0
  4. package/.nyc_output/processinfo/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +1 -0
  5. package/.nyc_output/processinfo/index.json +1 -1
  6. package/build/lib/mkdir.js +2 -2
  7. package/components/date/index.js +37 -24
  8. package/components/date/templates/date.html +3 -3
  9. package/components/emailer/index.js +49 -41
  10. package/components/emailer/transports/debug.js +1 -2
  11. package/components/summary/index.js +18 -0
  12. package/config/hof-defaults.js +5 -3
  13. package/config/rate-limits.js +20 -0
  14. package/config/sanitisation-rules.js +29 -0
  15. package/controller/base-controller.js +26 -8
  16. package/controller/controller.js +11 -15
  17. package/frontend/govuk-template/build/config.js +1 -1
  18. package/frontend/template-mixins/mixins/template-mixins.js +8 -8
  19. package/frontend/template-mixins/partials/forms/checkbox-group.html +3 -3
  20. package/frontend/template-mixins/partials/forms/input-text-group.html +3 -3
  21. package/frontend/template-mixins/partials/forms/option-group.html +3 -3
  22. package/frontend/template-mixins/partials/forms/select.html +2 -2
  23. package/frontend/template-mixins/partials/forms/textarea-group.html +2 -2
  24. package/frontend/template-mixins/partials/mixins/panel.html +3 -4
  25. package/frontend/template-partials/translations/src/en/errors.json +12 -0
  26. package/frontend/template-partials/views/rate-limit-error.html +10 -0
  27. package/frontend/themes/gov-uk/client-js/govuk-cookies.js +43 -44
  28. package/frontend/themes/gov-uk/client-js/index.js +2 -2
  29. package/frontend/themes/gov-uk/client-js/skip-to-main.js +18 -17
  30. package/frontend/themes/gov-uk/styles/_cookie-banner.scss +51 -1
  31. package/frontend/toolkit/assets/javascript/form-focus.js +10 -1
  32. package/frontend/toolkit/assets/javascript/validation.js +6 -1
  33. package/index.js +9 -4
  34. package/lib/router.js +2 -1
  35. package/lib/settings.js +18 -2
  36. package/middleware/errors.js +32 -0
  37. package/middleware/index.js +2 -1
  38. package/middleware/rate-limiter.js +98 -0
  39. package/package.json +5 -6
  40. package/sandbox/.env +1 -1
  41. package/sandbox/apps/sandbox/fields.js +14 -9
  42. package/sandbox/apps/sandbox/index.js +1 -5
  43. package/sandbox/apps/sandbox/translations/en/default.json +4 -13
  44. package/sandbox/apps/sandbox/translations/src/en/fields.json +3 -2
  45. package/sandbox/apps/sandbox/translations/src/en/pages.json +2 -12
  46. package/sandbox/assets/scss/app.scss +0 -55
  47. package/sandbox/public/css/app.css +52 -62
  48. package/sandbox/public/js/bundle.js +79 -65
  49. package/sandbox/server.js +5 -0
  50. package/transpiler/lib/write-files.js +1 -2
  51. package/utilities/helpers/index.js +16 -1
  52. package/wizard/index.js +1 -0
  53. package/.nyc_output/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
  54. package/.nyc_output/processinfo/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
@@ -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.1",
4
+ "version": "20.0.0-beta.10",
5
5
  "license": "MIT",
6
6
  "main": "index.js",
7
7
  "author": "HomeOffice",
@@ -70,10 +70,9 @@
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",
@@ -88,8 +87,8 @@
88
87
  "serve-static": "^1.14.1",
89
88
  "uglify-js": "^3.14.3",
90
89
  "underscore": "^1.12.1",
91
- "urijs": "^1.19.10",
92
- "winston": "^3.3.3"
90
+ "urijs": "^1.19.11",
91
+ "winston": "^3.7.2"
93
92
  },
94
93
  "devDependencies": {
95
94
  "@cucumber/cucumber": "^7.3.0",
package/sandbox/.env CHANGED
@@ -1 +1 @@
1
- GA_TAG=UA-215558609-1
1
+ // GA_TAG=UA-215558609-1
@@ -8,19 +8,20 @@ module.exports = {
8
8
  'landing-page-radio': {
9
9
  mixin: 'radio-group',
10
10
  validate: ['required'],
11
- legend: {
12
- className: 'visuallyhidden'
13
- },
11
+ isPageHeading: true,
12
+ // Design system says to avoid in-line unless it's two options,
13
+ // so just added as an example below but by default it isn't
14
14
  className: ['govuk-radios--inline'],
15
15
  options: ['basic-form', 'complex-form', 'build-your-own-form']
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
- controlType: 'date-input',
24
+ isPageHeading: 'true',
24
25
  validate: [
25
26
  'required',
26
27
  'date',
@@ -45,6 +46,7 @@ module.exports = {
45
46
  formatter: ['removespaces', 'uppercase']
46
47
  },
47
48
  incomeTypes: {
49
+ isPageHeading: 'true',
48
50
  mixin: 'checkbox-group',
49
51
  labelClassName: 'visuallyhidden',
50
52
  validate: ['required'],
@@ -57,11 +59,9 @@ module.exports = {
57
59
  ]
58
60
  },
59
61
  countryOfHearing: {
62
+ isPageHeading: 'true',
60
63
  mixin: 'radio-group',
61
64
  validate: ['required'],
62
- legend: {
63
- className: 'visuallyhidden'
64
- },
65
65
  options: [
66
66
  'englandAndWales',
67
67
  'scotland',
@@ -69,6 +69,8 @@ module.exports = {
69
69
  ]
70
70
  },
71
71
  email: {
72
+ isPageHeading: 'true',
73
+ labelClassName: ['govuk-label--l'],
72
74
  validate: ['required', 'email']
73
75
  },
74
76
  phone: {
@@ -80,6 +82,7 @@ module.exports = {
80
82
  },
81
83
  countrySelect: {
82
84
  mixin: 'select',
85
+ isPageHeading: 'true',
83
86
  className: ['typeahead'],
84
87
  options:[''].concat(require('homeoffice-countries').allCountries),
85
88
  legend: {
@@ -91,12 +94,13 @@ module.exports = {
91
94
  mixin: 'textarea',
92
95
  // we want to ignore default formatters as we want
93
96
  // to preserve white space
97
+ isPageHeading: 'true',
94
98
  'ignore-defaults': true,
95
99
  // apply the other default formatters
96
100
  formatter: ['trim', 'hyphens'],
97
101
  labelClassName: ['govuk-label--l'],
98
102
  // attributes here are passed to the field element
99
- validate: ['required', { type: 'maxlength', arguments: 100 }],
103
+ validate: ['required', { type: 'maxlength', arguments: 10 }],
100
104
  attributes: [{
101
105
  attribute: 'rows',
102
106
  value: 8
@@ -104,6 +108,7 @@ module.exports = {
104
108
  },
105
109
  appealStages: {
106
110
  mixin: 'select',
111
+ isPageHeading: 'true',
107
112
  validate: ['required'],
108
113
  options: [{
109
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'],
@@ -2,6 +2,7 @@
2
2
  "fields": {
3
3
  "landing-page-radio": {
4
4
  "legend": "Which form would you like to explore?",
5
+ "hint": "Choose one of the options below and press continue.",
5
6
  "options": {
6
7
  "basic-form": {
7
8
  "label": "Basic form"
@@ -56,7 +57,7 @@
56
57
  }
57
58
  },
58
59
  "countryOfHearing": {
59
- "label": "Country of hearing",
60
+ "legend": "What country was the appeal lodged?",
60
61
  "options": {
61
62
  "englandAndWales": {
62
63
  "label": "England and Wales"
@@ -70,7 +71,7 @@
70
71
  }
71
72
  },
72
73
  "email": {
73
- "label": "Email address"
74
+ "label": "Enter your email address"
74
75
  },
75
76
  "phone": {
76
77
  "label": "Phone number",
@@ -103,8 +104,7 @@
103
104
  },
104
105
  "pages": {
105
106
  "landing-page": {
106
- "header": "Landing page",
107
- "intro": "Choose one of the options below and press continue."
107
+ "header": "Landing page"
108
108
  },
109
109
  "build-your-own-form": {
110
110
  "title": "Build your own form",
@@ -114,15 +114,6 @@
114
114
  "header": "What is your address in the UK?",
115
115
  "intro": "If you have no fixed address, enter an address where we can contact you."
116
116
  },
117
- "checkboxes": {
118
- "header": "Where does your money come from each month?"
119
- },
120
- "radio": {
121
- "header": "What country was the appeal lodged?"
122
- },
123
- "email": {
124
- "header": "Enter your email address"
125
- },
126
117
  "phone-number": {
127
118
  "header": "Enter your phone number"
128
119
  },
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "landing-page-radio": {
3
3
  "legend": "Which form would you like to explore?",
4
+ "hint": "Choose one of the options below and press continue.",
4
5
  "options": {
5
6
  "basic-form": {
6
7
  "label": "Basic form"
@@ -55,7 +56,7 @@
55
56
  }
56
57
  },
57
58
  "countryOfHearing": {
58
- "label": "Country of hearing",
59
+ "legend": "What country was the appeal lodged?",
59
60
  "options": {
60
61
  "englandAndWales": {
61
62
  "label": "England and Wales"
@@ -69,7 +70,7 @@
69
70
  }
70
71
  },
71
72
  "email" : {
72
- "label": "Email address"
73
+ "label": "Enter your email address"
73
74
  },
74
75
  "phone": {
75
76
  "label": "Phone number",
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "landing-page": {
3
- "header": "Landing page",
4
- "intro": "Choose one of the options below and press continue."
3
+ "header": "Landing page"
5
4
  },
6
5
  "build-your-own-form": {
7
6
  "title": "Build your own form",
@@ -11,15 +10,6 @@
11
10
  "header": "What is your address in the UK?",
12
11
  "intro": "If you have no fixed address, enter an address where we can contact you."
13
12
  },
14
- "checkboxes": {
15
- "header": "Where does your money come from each month?"
16
- },
17
- "radio": {
18
- "header": "What country was the appeal lodged?"
19
- },
20
- "email": {
21
- "header": "Enter your email address"
22
- },
23
13
  "phone-number": {
24
14
  "header": "Enter your phone number"
25
15
  },
@@ -55,4 +45,4 @@
55
45
  "subheader": "What happens next",
56
46
  "content": "We’ll contact you with the decision of your application or if we need more information from you."
57
47
  }
58
- }
48
+ }
@@ -26,58 +26,3 @@
26
26
  .twitter-typeahead {
27
27
  width: 100%;
28
28
  }
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
- }
80
-
81
- #cookie-banner {
82
- max-width: none;
83
- }
@@ -2239,7 +2239,6 @@ strong {
2239
2239
  }
2240
2240
 
2241
2241
  #cookie-banner {
2242
- max-width: 960px;
2243
2242
  margin: 0 15px;
2244
2243
  }
2245
2244
  #cookie-banner p {
@@ -2294,6 +2293,56 @@ strong {
2294
2293
  height: fit-content;
2295
2294
  }
2296
2295
 
2296
+ .govuk-banner--success {
2297
+ border-color: #00703c;
2298
+ color: #00703c;
2299
+ }
2300
+
2301
+ .govuk-banner {
2302
+ border: 5px solid #1d70b8;
2303
+ font-size: 0;
2304
+ margin-bottom: 30px;
2305
+ padding: 10px;
2306
+ }
2307
+
2308
+ .govuk-banner__icon {
2309
+ display: inline-block;
2310
+ }
2311
+
2312
+ .govuk-banner__message {
2313
+ font-family: "GDS Transport", Arial, sans-serif;
2314
+ -webkit-font-smoothing: antialiased;
2315
+ font-weight: 400;
2316
+ font-size: 1rem;
2317
+ line-height: 1.25;
2318
+ color: #0b0c0c;
2319
+ display: block;
2320
+ overflow: hidden;
2321
+ display: inline-block;
2322
+ margin-left: 10px;
2323
+ }
2324
+
2325
+ .govuk-banner__assistive {
2326
+ position: absolute !important;
2327
+ width: 1px !important;
2328
+ height: 1px !important;
2329
+ margin: 0 !important;
2330
+ padding: 0 !important;
2331
+ overflow: hidden !important;
2332
+ clip: rect(0 0 0 0) !important;
2333
+ clip-path: inset(50%) !important;
2334
+ border: 0 !important;
2335
+ white-space: nowrap !important;
2336
+ }
2337
+
2338
+ .cookie-table-holder > table > tbody > tr > td:first-child {
2339
+ font-weight: bold;
2340
+ }
2341
+
2342
+ .js-enabled #global-cookie-message {
2343
+ display: none;
2344
+ }
2345
+
2297
2346
  #cookie-settings .js-enabled {
2298
2347
  display: none;
2299
2348
  }
@@ -2709,8 +2758,7 @@ fieldset + .reveal {
2709
2758
 
2710
2759
  .alert-number {
2711
2760
  float: left;
2712
- margin-right: 0.41666667em;
2713
- /* 20px ÷ 48px font size */
2761
+ margin-right: 0.41666667em; /* 20px ÷ 48px font size */
2714
2762
  }
2715
2763
 
2716
2764
  .alert-message {
@@ -2778,8 +2826,7 @@ fieldset + .reveal {
2778
2826
  -moz-osx-font-smoothing: grayscale;
2779
2827
  text-decoration: underline;
2780
2828
  }
2781
- /*! Copyright (c) 2011 by Margaret Calvert & Henrik Kubel. All rights reserved. The font has been customised for exclusive use on gov.uk. This cut is not commercially available. */
2782
- /* stylelint-disable-line scss/comment-no-loud */
2829
+ /*! Copyright (c) 2011 by Margaret Calvert & Henrik Kubel. All rights reserved. The font has been customised for exclusive use on gov.uk. This cut is not commercially available. */ /* stylelint-disable-line scss/comment-no-loud */
2783
2830
  @font-face {
2784
2831
  font-family: "GDS Transport";
2785
2832
  font-style: normal;
@@ -6256,7 +6303,6 @@ x:-moz-any-link {
6256
6303
  -webkit-column-count: 2;
6257
6304
  column-count: 2;
6258
6305
  }
6259
-
6260
6306
  .govuk-footer__list--columns-3 {
6261
6307
  -webkit-column-count: 3;
6262
6308
  column-count: 3;
@@ -6688,11 +6734,9 @@ x:-moz-any-link {
6688
6734
  color: #0b0c0c;
6689
6735
  background: transparent;
6690
6736
  }
6691
-
6692
6737
  .govuk-header__logotype-crown-fallback-image {
6693
6738
  display: none;
6694
6739
  }
6695
-
6696
6740
  .govuk-header__link:link, .govuk-header__link:visited {
6697
6741
  color: #0b0c0c;
6698
6742
  }
@@ -9342,58 +9386,4 @@ x:-moz-any-link {
9342
9386
 
9343
9387
  .twitter-typeahead {
9344
9388
  width: 100%;
9345
- }
9346
-
9347
- .govuk-banner--success {
9348
- border-color: #00703c;
9349
- color: #00703c;
9350
- }
9351
-
9352
- .govuk-banner {
9353
- border: 5px solid #1d70b8;
9354
- font-size: 0;
9355
- margin-bottom: 30px;
9356
- padding: 10px;
9357
- }
9358
-
9359
- .govuk-banner__icon {
9360
- display: inline-block;
9361
- }
9362
-
9363
- .govuk-banner__message {
9364
- font-family: "GDS Transport", Arial, sans-serif;
9365
- -webkit-font-smoothing: antialiased;
9366
- font-weight: 400;
9367
- font-size: 1rem;
9368
- line-height: 1.25;
9369
- color: #0b0c0c;
9370
- display: block;
9371
- overflow: hidden;
9372
- display: inline-block;
9373
- margin-left: 10px;
9374
- }
9375
-
9376
- .govuk-banner__assistive {
9377
- position: absolute !important;
9378
- width: 1px !important;
9379
- height: 1px !important;
9380
- margin: 0 !important;
9381
- padding: 0 !important;
9382
- overflow: hidden !important;
9383
- clip: rect(0 0 0 0) !important;
9384
- clip-path: inset(50%) !important;
9385
- border: 0 !important;
9386
- white-space: nowrap !important;
9387
- }
9388
-
9389
- .cookie-table-holder > table > tbody > tr > td:first-child {
9390
- font-weight: bold;
9391
- }
9392
-
9393
- .js-enabled #global-cookie-message {
9394
- display: none;
9395
- }
9396
-
9397
- #cookie-banner {
9398
- max-width: none;
9399
9389
  }