@gov-cy/govcy-express-services 1.3.0-alpha.1 → 1.3.0-alpha.3
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/README.md +250 -371
- package/package.json +2 -2
- package/src/index.mjs +11 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +8 -0
- package/src/middleware/govcyFileViewHandler.mjs +5 -0
- package/src/middleware/govcyMultipleThingsDeleteHandler.mjs +2 -2
- package/src/middleware/govcyMultipleThingsHubHandler.mjs +0 -1
- package/src/middleware/govcyPageHandler.mjs +11 -4
- package/src/middleware/govcyReviewPostHandler.mjs +24 -3
- package/src/middleware/govcyUpdateMyDetails.mjs +744 -0
- package/src/public/css/govcyExpress.css +4 -0
- package/src/resources/govcyResources.mjs +471 -95
- package/src/utils/govcyConstants.mjs +13 -1
- package/src/utils/govcyHandleFiles.mjs +10 -0
- package/src/utils/govcySubmitData.mjs +45 -6
- package/src/utils/govcyValidator.mjs +104 -5
|
@@ -5,4 +5,16 @@ export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios
|
|
|
5
5
|
export const ALLOWED_FILE_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
|
|
6
6
|
export const ALLOWED_FILE_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png'];
|
|
7
7
|
export const ALLOWED_FILE_SIZE_MB = 4; // Maximum file size in MB
|
|
8
|
-
export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // Maximum file size in MB
|
|
8
|
+
export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // Maximum file size in MB
|
|
9
|
+
// UPDATE MY DETAILS
|
|
10
|
+
// Only allow certain hosts
|
|
11
|
+
export const UPDATE_MY_DETAILS_ALLOWED_HOSTS = [
|
|
12
|
+
"update-my-details.staging.service.gov.cy",
|
|
13
|
+
"update-my-details.service.gov.cy",
|
|
14
|
+
"localhost"
|
|
15
|
+
];
|
|
16
|
+
// Possible incoming routes
|
|
17
|
+
export const UPDATE_MY_DETAILS_REDIRECT_HOSTS = [
|
|
18
|
+
"update-my-details.staging.service.gov.cy",
|
|
19
|
+
"update-my-details.service.gov.cy"
|
|
20
|
+
];
|
|
@@ -100,6 +100,16 @@ export async function handleFileUpload({
|
|
|
100
100
|
|
|
101
101
|
// deep copy the page template to avoid modifying the original
|
|
102
102
|
const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
|
|
103
|
+
|
|
104
|
+
// If mode is `single` make sure it has no multipleThings
|
|
105
|
+
if (mode === "single" && page?.multipleThings) {
|
|
106
|
+
return {
|
|
107
|
+
status: 400,
|
|
108
|
+
dataStatus: 413,
|
|
109
|
+
errorMessage: 'Single mode upload not allowed on multipleThings pages'
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
103
113
|
// Validate the field: Only allow upload if the page contains a fileInput with the given name
|
|
104
114
|
const isAllowed = pageContainsFileInput(pageTemplateCopy, elementName);
|
|
105
115
|
if (!isAllowed) {
|
|
@@ -6,6 +6,7 @@ import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
|
|
|
6
6
|
import { evaluatePageConditions } from "./govcyExpressions.mjs";
|
|
7
7
|
import { getPageConfigData } from "./govcyLoadConfigData.mjs";
|
|
8
8
|
import { logger } from "./govcyLogger.mjs";
|
|
9
|
+
import { createUmdManualPageTemplate } from "../middleware/govcyUpdateMyDetails.mjs"
|
|
9
10
|
import nunjucks from "nunjucks";
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -43,9 +44,30 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
43
44
|
|
|
44
45
|
// Find the <form> element in the page
|
|
45
46
|
let formElement = null;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
|
|
48
|
+
// ----- `updateMyDetails` handling
|
|
49
|
+
// 🔹 Case C: updateMyDetails
|
|
50
|
+
if (page.updateMyDetails) {
|
|
51
|
+
logger.debug("Preparing submission data for UpdateMyDetails page", { siteId, pageUrl });
|
|
52
|
+
// Build the manual UMD page template
|
|
53
|
+
const umdTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
|
|
54
|
+
|
|
55
|
+
// Extract the form element
|
|
56
|
+
formElement = umdTemplate .sections
|
|
57
|
+
.flatMap(section => section.elements)
|
|
58
|
+
.find(el => el.element === "form");
|
|
59
|
+
|
|
60
|
+
if (!formElement) {
|
|
61
|
+
logger.error("🚨 UMD form element not found during prepareSubmissionData", { siteId, pageUrl });
|
|
62
|
+
return handleMiddlewareError("🚨 UMD form element not found during prepareSubmissionData", 500, next);
|
|
63
|
+
}
|
|
64
|
+
// ----- `updateMyDetails` handling
|
|
65
|
+
} else {
|
|
66
|
+
// Normal flow
|
|
67
|
+
for (const section of page.pageTemplate.sections || []) {
|
|
68
|
+
formElement = section.elements.find(el => el.element === "form");
|
|
69
|
+
if (formElement) break;
|
|
70
|
+
}
|
|
49
71
|
}
|
|
50
72
|
|
|
51
73
|
if (!formElement) continue; // ⛔ Skip pages without a <form> element
|
|
@@ -235,8 +257,20 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
235
257
|
continue; // ⛔ Skip this page from print-friendly data
|
|
236
258
|
}
|
|
237
259
|
|
|
260
|
+
let pageTemplate = page.pageTemplate;
|
|
261
|
+
let pageTitle = page.pageData.title || {};
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
// ----- MultipleThings hub handling
|
|
265
|
+
if (page.updateMyDetails) {
|
|
266
|
+
// create the page template
|
|
267
|
+
pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req );
|
|
268
|
+
// set the page title
|
|
269
|
+
pageTitle = govcyResources.staticResources.text.updateMyDetailsTitle;
|
|
270
|
+
}
|
|
271
|
+
|
|
238
272
|
// find the form element in the page template
|
|
239
|
-
for (const section of
|
|
273
|
+
for (const section of pageTemplate.sections || []) {
|
|
240
274
|
for (const element of section.elements || []) {
|
|
241
275
|
if (element.element !== "form") continue;
|
|
242
276
|
|
|
@@ -282,6 +316,7 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
282
316
|
|
|
283
317
|
// Special case: multipleThings page → extract item titles // ✅ new
|
|
284
318
|
if (page.multipleThings) {
|
|
319
|
+
pageTitle = page.multipleThings?.listPage?.title || pageTitle;
|
|
285
320
|
let mtItems = dataLayer.getPageData(req.session, siteId, page.pageData.url);
|
|
286
321
|
if (Array.isArray(mtItems)) {
|
|
287
322
|
const env = new nunjucks.Environment(null, { autoescape: false });
|
|
@@ -295,7 +330,7 @@ export function preparePrintFriendlyData(req, siteId, service) {
|
|
|
295
330
|
if (fields.length > 0) {
|
|
296
331
|
submissionData.push({
|
|
297
332
|
pageUrl: page.pageData.url,
|
|
298
|
-
pageTitle:
|
|
333
|
+
pageTitle: pageTitle,
|
|
299
334
|
fields,
|
|
300
335
|
items: (page.multipleThings ? items : null) // ✅ new
|
|
301
336
|
});
|
|
@@ -674,8 +709,12 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
|
|
|
674
709
|
// For each page in the submission data
|
|
675
710
|
for (const page of submissionData) {
|
|
676
711
|
// Get the page URL, title, and fields
|
|
677
|
-
|
|
712
|
+
let { pageUrl, pageTitle, fields, items } = page;
|
|
678
713
|
|
|
714
|
+
// 🔹 MultipleThings page
|
|
715
|
+
if (page.multipleThings) {
|
|
716
|
+
pageTitle = page.multipleThings?.listPage?.title || pageTitle;
|
|
717
|
+
}
|
|
679
718
|
// Add data title to the body
|
|
680
719
|
body.push(
|
|
681
720
|
{
|
|
@@ -96,13 +96,39 @@ function validateValue(value, rules) {
|
|
|
96
96
|
}
|
|
97
97
|
return normalizedVal <= max;
|
|
98
98
|
},
|
|
99
|
-
// ✅
|
|
99
|
+
// ✅ Year based current rules
|
|
100
100
|
maxCurrentYear: (val) => {
|
|
101
101
|
const normalizedVal = normalizeNumber(val);
|
|
102
102
|
if (isNaN(normalizedVal)) return false;
|
|
103
103
|
const currentYear = new Date().getFullYear();
|
|
104
104
|
return normalizedVal <= currentYear;
|
|
105
105
|
},
|
|
106
|
+
minCurrentYear: (val) => {
|
|
107
|
+
const normalizedVal = normalizeNumber(val);
|
|
108
|
+
if (isNaN(normalizedVal)) return false;
|
|
109
|
+
const currentYear = new Date().getFullYear();
|
|
110
|
+
return normalizedVal >= currentYear;
|
|
111
|
+
},
|
|
112
|
+
// ✅ Date-based current rules
|
|
113
|
+
minCurrentDate: (val) => {
|
|
114
|
+
const valueDate = parseDate(val);
|
|
115
|
+
const today = new Date();
|
|
116
|
+
if (isNaN(valueDate)) return false;
|
|
117
|
+
// strip time components from both
|
|
118
|
+
const valueOnly = new Date(valueDate.getFullYear(), valueDate.getMonth(), valueDate.getDate());
|
|
119
|
+
const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
120
|
+
return valueOnly >= todayOnly;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
maxCurrentDate: (val) => {
|
|
124
|
+
const valueDate = parseDate(val);
|
|
125
|
+
const today = new Date();
|
|
126
|
+
if (isNaN(valueDate)) return false;
|
|
127
|
+
const valueOnly = new Date(valueDate.getFullYear(), valueDate.getMonth(), valueDate.getDate());
|
|
128
|
+
const todayOnly = new Date(today.getFullYear(), today.getMonth(), today.getDate());
|
|
129
|
+
return valueOnly <= todayOnly;
|
|
130
|
+
},
|
|
131
|
+
|
|
106
132
|
minValueDate: (val, minDate) => {
|
|
107
133
|
const valueDate = parseDate(val); // Parse the input date
|
|
108
134
|
const min = parseDate(minDate); // Parse the minimum date
|
|
@@ -139,7 +165,7 @@ function validateValue(value, rules) {
|
|
|
139
165
|
// Skip validation if the value is empty
|
|
140
166
|
if (value === null || value === undefined || (typeof value === 'string' && value.trim() === "")) {
|
|
141
167
|
continue; // let "required" handle emptiness
|
|
142
|
-
}
|
|
168
|
+
}
|
|
143
169
|
// Check for "valid" rules (e.g., numeric, telCY, etc.)
|
|
144
170
|
if (check === "valid" && validationRules[checkValue]) {
|
|
145
171
|
const isValid = validationRules[checkValue](value);
|
|
@@ -168,12 +194,12 @@ function validateValue(value, rules) {
|
|
|
168
194
|
if (check === 'minValue' && !validationRules.minValue(value, checkValue)) {
|
|
169
195
|
return message;
|
|
170
196
|
}
|
|
171
|
-
|
|
197
|
+
|
|
172
198
|
// Check for "maxValue"
|
|
173
199
|
if (check === 'maxValue' && !validationRules.maxValue(value, checkValue)) {
|
|
174
200
|
return message;
|
|
175
201
|
}
|
|
176
|
-
|
|
202
|
+
|
|
177
203
|
// Check for "minValueDate"
|
|
178
204
|
if (check === 'minValueDate' && !validationRules.minValueDate(value, checkValue)) {
|
|
179
205
|
return message;
|
|
@@ -368,4 +394,77 @@ export function validateFormElements(elements, formData, pageUrl) {
|
|
|
368
394
|
}
|
|
369
395
|
});
|
|
370
396
|
return validationErrors;
|
|
371
|
-
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* Checks if a user is an Individual with a valid Cypriot citizen identifier.
|
|
402
|
+
* Rules:
|
|
403
|
+
* - profile_type must be "Individual"
|
|
404
|
+
* - unique_identifier must be a string
|
|
405
|
+
* - must start with "00"
|
|
406
|
+
* - must be 10 characters long
|
|
407
|
+
*
|
|
408
|
+
* @param {object} user - The user object (e.g. req.session.user)
|
|
409
|
+
* @returns {boolean} true if valid, false otherwise
|
|
410
|
+
*/
|
|
411
|
+
export function isValidCypriotCitizen(user = {}) {
|
|
412
|
+
const { profile_type, unique_identifier } = user;
|
|
413
|
+
|
|
414
|
+
if (
|
|
415
|
+
typeof profile_type === "string" &&
|
|
416
|
+
profile_type === "Individual" &&
|
|
417
|
+
typeof unique_identifier === "string" &&
|
|
418
|
+
unique_identifier.startsWith("00") &&
|
|
419
|
+
unique_identifier.length === 10
|
|
420
|
+
) {
|
|
421
|
+
return true;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Checks if the given user represents a valid foreign resident (ARC holder).
|
|
429
|
+
* Conditions:
|
|
430
|
+
* - profile_type must equal "Individual"
|
|
431
|
+
* - unique_identifier must be a string
|
|
432
|
+
* - unique_identifier must start with "05"
|
|
433
|
+
* - unique_identifier must be exactly 10 characters long
|
|
434
|
+
*
|
|
435
|
+
* @param {object} user - e.g. req.session.user
|
|
436
|
+
* @returns {boolean} True if valid foreign resident, otherwise false
|
|
437
|
+
*/
|
|
438
|
+
export function isValidForeignResident(user = {}) {
|
|
439
|
+
const { profile_type, unique_identifier } = user;
|
|
440
|
+
|
|
441
|
+
return (
|
|
442
|
+
typeof profile_type === "string" &&
|
|
443
|
+
profile_type === "Individual" &&
|
|
444
|
+
typeof unique_identifier === "string" &&
|
|
445
|
+
unique_identifier.startsWith("05") &&
|
|
446
|
+
unique_identifier.length === 10
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* Checks if the user is under 18 years old based on their date of birth.
|
|
452
|
+
* @param {string} dobString - The date of birth in the format "YYYY-MM-DD".
|
|
453
|
+
* @returns {boolean} True if the user is under 18 years old, otherwise false.
|
|
454
|
+
* @throws {Error} If the date of birth is missing or invalid.
|
|
455
|
+
* */
|
|
456
|
+
export function isUnder18(dobString) {
|
|
457
|
+
if (!dobString) throw new Error("DOB is missing");
|
|
458
|
+
const dob = new Date(dobString);
|
|
459
|
+
if (isNaN(dob)) throw new Error("Invalid DOB format");
|
|
460
|
+
|
|
461
|
+
const today = new Date();
|
|
462
|
+
const ageDiff = today.getFullYear() - dob.getFullYear();
|
|
463
|
+
const hasHadBirthday =
|
|
464
|
+
today.getMonth() > dob.getMonth() ||
|
|
465
|
+
(today.getMonth() === dob.getMonth() && today.getDate() >= dob.getDate());
|
|
466
|
+
|
|
467
|
+
return (hasHadBirthday ? ageDiff : ageDiff - 1) < 18;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
|