hof 20.0.0-beta.1 → 20.0.0-beta.12
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/.github/workflows/automate-publish.yml +1 -1
- package/.github/workflows/automate-tag.yml +1 -1
- package/.nyc_output/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +1 -0
- package/.nyc_output/processinfo/e2fdc3eb-4fd2-47e0-a392-fe5f665776a4.json +1 -0
- package/.nyc_output/processinfo/index.json +1 -1
- package/build/lib/mkdir.js +2 -2
- package/components/date/index.js +37 -24
- package/components/date/templates/date.html +3 -3
- package/components/emailer/index.js +49 -41
- package/components/emailer/transports/debug.js +1 -2
- package/components/summary/index.js +18 -0
- package/config/hof-defaults.js +5 -3
- package/config/rate-limits.js +20 -0
- package/config/sanitisation-rules.js +29 -0
- package/controller/base-controller.js +26 -8
- package/controller/controller.js +11 -15
- package/frontend/govuk-template/build/config.js +1 -1
- package/frontend/template-mixins/mixins/template-mixins.js +8 -8
- package/frontend/template-mixins/partials/forms/checkbox-group.html +3 -3
- package/frontend/template-mixins/partials/forms/input-text-group.html +3 -3
- package/frontend/template-mixins/partials/forms/option-group.html +3 -3
- package/frontend/template-mixins/partials/forms/select.html +2 -2
- package/frontend/template-mixins/partials/forms/textarea-group.html +2 -2
- package/frontend/template-mixins/partials/mixins/panel.html +3 -4
- package/frontend/template-partials/translations/src/en/errors.json +12 -0
- package/frontend/template-partials/views/partials/form.html +2 -1
- package/frontend/template-partials/views/rate-limit-error.html +10 -0
- package/frontend/themes/gov-uk/client-js/govuk-cookies.js +43 -44
- package/frontend/themes/gov-uk/client-js/index.js +2 -2
- package/frontend/themes/gov-uk/client-js/skip-to-main.js +18 -17
- package/frontend/themes/gov-uk/styles/_cookie-banner.scss +51 -1
- package/frontend/themes/gov-uk/styles/govuk.scss +5 -0
- package/frontend/toolkit/assets/javascript/form-focus.js +10 -1
- package/frontend/toolkit/assets/javascript/validation.js +6 -1
- package/index.js +9 -4
- package/lib/router.js +2 -1
- package/lib/settings.js +18 -2
- package/middleware/errors.js +32 -0
- package/middleware/index.js +2 -1
- package/middleware/rate-limiter.js +98 -0
- package/package.json +5 -6
- package/sandbox/.env +1 -1
- package/sandbox/apps/sandbox/fields.js +14 -9
- package/sandbox/apps/sandbox/index.js +1 -5
- package/sandbox/apps/sandbox/translations/en/default.json +4 -13
- package/sandbox/apps/sandbox/translations/src/en/fields.json +3 -2
- package/sandbox/apps/sandbox/translations/src/en/pages.json +2 -12
- package/sandbox/assets/scss/app.scss +0 -55
- package/sandbox/package.json +1 -0
- package/sandbox/public/css/app.css +4898 -4908
- package/sandbox/public/js/bundle.js +79 -65
- package/sandbox/server.js +7 -1
- package/sandbox/yarn.lock +20 -1
- package/transpiler/lib/write-files.js +1 -2
- package/utilities/helpers/index.js +16 -1
- package/wizard/index.js +1 -0
- package/.nyc_output/65af88d9-aebe-4d1b-a21d-6fbf7f2bbda4.json +0 -1
- 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.
|
4
|
+
"version": "20.0.0-beta.12",
|
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.
|
73
|
+
"minimist": "^1.2.6",
|
74
74
|
"mixwith": "^0.1.1",
|
75
|
-
"
|
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.
|
92
|
-
"winston": "^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
|
-
|
12
|
-
|
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
|
-
|
21
|
+
isPageHeading: 'true'
|
21
22
|
},
|
22
23
|
'dateOfBirth': dateComponent('dateOfBirth', {
|
23
|
-
|
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:
|
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
|
-
"
|
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": "
|
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
|
-
"
|
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": "
|
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
|
-
}
|