@gov-cy/govcy-express-services 0.1.1

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 (41) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1157 -0
  3. package/package.json +72 -0
  4. package/src/auth/cyLoginAuth.mjs +123 -0
  5. package/src/index.mjs +188 -0
  6. package/src/middleware/cyLoginAuth.mjs +131 -0
  7. package/src/middleware/govcyConfigSiteData.mjs +38 -0
  8. package/src/middleware/govcyCsrf.mjs +36 -0
  9. package/src/middleware/govcyFormsPostHandler.mjs +83 -0
  10. package/src/middleware/govcyHeadersControl.mjs +22 -0
  11. package/src/middleware/govcyHttpErrorHandler.mjs +63 -0
  12. package/src/middleware/govcyLanguageMiddleware.mjs +19 -0
  13. package/src/middleware/govcyLogger.mjs +15 -0
  14. package/src/middleware/govcyManifestHandler.mjs +46 -0
  15. package/src/middleware/govcyPDFRender.mjs +30 -0
  16. package/src/middleware/govcyPageHandler.mjs +110 -0
  17. package/src/middleware/govcyPageRender.mjs +14 -0
  18. package/src/middleware/govcyRequestTimer.mjs +29 -0
  19. package/src/middleware/govcyReviewPageHandler.mjs +102 -0
  20. package/src/middleware/govcyReviewPostHandler.mjs +147 -0
  21. package/src/middleware/govcyRoutePageHandler.mjs +37 -0
  22. package/src/middleware/govcyServiceEligibilityHandler.mjs +101 -0
  23. package/src/middleware/govcySessionData.mjs +9 -0
  24. package/src/middleware/govcySuccessPageHandler.mjs +112 -0
  25. package/src/public/img/Certificate_A4.svg +30 -0
  26. package/src/public/js/govcyForms.js +21 -0
  27. package/src/resources/govcyResources.mjs +430 -0
  28. package/src/standalone.mjs +7 -0
  29. package/src/utils/govcyApiRequest.mjs +114 -0
  30. package/src/utils/govcyConstants.mjs +4 -0
  31. package/src/utils/govcyDataLayer.mjs +311 -0
  32. package/src/utils/govcyEnvVariables.mjs +45 -0
  33. package/src/utils/govcyFormHandling.mjs +148 -0
  34. package/src/utils/govcyLoadConfigData.mjs +135 -0
  35. package/src/utils/govcyLogger.mjs +30 -0
  36. package/src/utils/govcyNotification.mjs +85 -0
  37. package/src/utils/govcyPdfMaker.mjs +27 -0
  38. package/src/utils/govcyReviewSummary.mjs +205 -0
  39. package/src/utils/govcySubmitData.mjs +530 -0
  40. package/src/utils/govcyUtils.mjs +13 -0
  41. package/src/utils/govcyValidator.mjs +352 -0
@@ -0,0 +1,352 @@
1
+ /**
2
+ * @module govcyValidator
3
+ * @fileoverview This module provides validation functions for form elements.
4
+ * It includes a function to validate form elements based on specified rules and conditions.
5
+ * It also handles conditional elements and checks for specific input types.
6
+ * Validation Types Breakdown:
7
+ * - `required`: Checks if the value is not null, undefined, or an empty string (after trimming).
8
+ * - `valid`: Executes the appropriate validation based on the checkValue (e.g., numeric, telCY, etc.).
9
+ * - `length`: Ensures that the value's length doesn't exceed the specified limit.
10
+ * - `regCheck`: Performs custom regex validation as per the rule's checkValue.
11
+ */
12
+ import * as govcyResources from "../resources/govcyResources.mjs";
13
+ import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
14
+
15
+ /**
16
+ * This function validates a value based on the provided rules.
17
+ *
18
+ * @param {string} value The value to validate
19
+ * @param {Array} rules The validation rules to apply
20
+ * @returns Error message text object if validation fails, otherwise null
21
+ */
22
+ function validateValue(value, rules) {
23
+ const validationRules = {
24
+ // Valid validation rules
25
+ numeric: (val) => /^\d+$/.test(val),
26
+ numDecimal: (val) => /^\d+(,\d+)?$/.test(val),
27
+ currency: (val) => /^\d+(,\d{1,2})?$/.test(val),
28
+ alpha: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff\s]+$/.test(val),
29
+ alphaNum: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff0-9\s]+$/.test(val),
30
+ noSpecialChars: (val) => /^([0-9]|[A-Z]|[a-z]|[α-ω]|[Α-Ω]|[,]|[.]|[-]|[(]|[)]|[?]|[!]|[;]|[:]|[\n]|[\r]|[ _]|[\u0370-\u03ff\u1f00-\u1fff])+$/.test(val),
31
+ name: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff\s'-]+$/.test(val),
32
+ tel: (val) => /^(?:\+|00)?[\d\s\-()]{8,20}$/.test(val.replace(/[\s\-()]/g, '')),
33
+ mobile: (val) => /^(?:\+|00)?[\d\s\-()]{8,20}$/.test(val.replace(/[\s\-()]/g, '')),
34
+ // telCY: (val) => /^(?:\+|00)?357[-\s]?(2|9)\d{7}$/.test(val.replace(/[\s\-()]/g, '')),
35
+ // telCY: (val) => /^(?:\+|00)?357?(2|9)\d{7}$/.test(val.replace(/[\s\-()]/g, '')),
36
+ telCY: (val) => {
37
+ const normalized = val.replace(/[\s\-()]/g, '');
38
+ const isValid = /^(?:\+357|00357)?(2|9)\d{7}$/.test(normalized);
39
+ return isValid;
40
+ },
41
+ // mobileCY: (val) => /^(?:\+|00)?357[-\s]?9\d{7}$/.test(val.replace(/[\s\-()]/g, '')),
42
+ mobileCY: (val) => {
43
+ const normalized = val.replace(/[\s\-()]/g, ''); // Remove spaces, hyphens, and parentheses
44
+ return /^(?:\+357|00357)?9\d{7}$/.test(normalized); // Match Cypriot mobile numbers
45
+ },
46
+ iban: (val) => {
47
+ const cleanedIBAN = val.replace(/[\s-]/g, '').toUpperCase(); // Remove spaces/hyphens and convert to uppercase
48
+ const regex = /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/;
49
+
50
+ // Validate structure and checksum
51
+ return regex.test(cleanedIBAN) && validateIBANChecksum(cleanedIBAN);
52
+ },
53
+ email: (val) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(val),
54
+ date: (val) => !isNaN(Date.parse(val)),
55
+ dateISO: (val) => {
56
+ if (!/^\d{4}-\d{1,2}-\d{1,2}$/.test(val)) return false; // Basic format check
57
+
58
+ const [year, month, day] = val.split("-").map(Number);
59
+ const date = new Date(year, month - 1, day); // JavaScript months are 0-based
60
+
61
+ return (
62
+ date.getFullYear() === year &&
63
+ date.getMonth() === month - 1 &&
64
+ date.getDate() === day
65
+ );
66
+ },
67
+ dateDMY: (val) => {
68
+ if (!/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(val)) return false; // First check format
69
+
70
+ const [day, month, year] = val.split('/').map(Number); // Convert to numbers
71
+ const date = new Date(year, month - 1, day); // Month is zero-based in JS
72
+
73
+ // Validate actual date parts
74
+ return (
75
+ date.getFullYear() === year &&
76
+ date.getMonth() === month - 1 &&
77
+ date.getDate() === day
78
+ );
79
+ },
80
+ // Other validation rules
81
+ length: (val, length) => val.length <= length,
82
+ required: (val) => !(val === null || val === undefined || (typeof val === 'string' && val.trim() === "")),
83
+ regCheck: (val, regex) => new RegExp(regex).test(val),
84
+ //Min and Max
85
+ minValue: (val, min) => {
86
+ const normalizedVal = normalizeNumber(val); // Normalize the input
87
+ if (isNaN(normalizedVal)) {
88
+ return false; // Return false if val cannot be converted to a number
89
+ }
90
+ return normalizedVal >= min;
91
+ },
92
+ maxValue: (val, max) => {
93
+ const normalizedVal = normalizeNumber(val); // Normalize the input
94
+ if (isNaN(normalizedVal)) {
95
+ return false; // Return false if val cannot be converted to a number
96
+ }
97
+ return normalizedVal <= max;
98
+ },
99
+ minValueDate: (val, minDate) => {
100
+ const valueDate = parseDate(val); // Parse the input date
101
+ const min = parseDate(minDate); // Parse the minimum date
102
+ if (isNaN(valueDate) || isNaN(min)) {
103
+ return false; // Return false if either date is invalid
104
+ }
105
+ return valueDate >= min;
106
+ },
107
+ maxValueDate: (val, maxDate) => {
108
+ const valueDate = parseDate(val); // Parse the input date
109
+ const max = parseDate(maxDate); // Parse the maximum date
110
+ if (isNaN(valueDate) || isNaN(max)) {
111
+ return false; // Return false if either date is invalid
112
+ }
113
+ return valueDate <= max;
114
+ },
115
+ minLength: (val, min) => val.length >= min
116
+ };
117
+
118
+ for (const rule of rules) {
119
+ // Extract rule parameters
120
+ const { check, params } = rule;
121
+ // Extract rule parameters
122
+ const { checkValue, message } = params;
123
+
124
+ // Handle "required" rules (check if value is not empty, null, or undefined)
125
+ if (check === "required") {
126
+ const isValid = validationRules.required(value);
127
+ if (!isValid) {
128
+ return message;
129
+ }
130
+ }
131
+
132
+ // Check for "valid" rules (e.g., numeric, telCY, etc.)
133
+ if (check === "valid" && validationRules[checkValue]) {
134
+ const isValid = validationRules[checkValue](value);
135
+ if (!isValid) {
136
+ return message;
137
+ }
138
+ }
139
+
140
+ // Check for "length" rules (e.g., max length check)
141
+ if (check === "length") {
142
+ const isValid = validationRules.length(value, checkValue);
143
+ if (!isValid) {
144
+ return message;
145
+ }
146
+ }
147
+
148
+ // Check for "regCheck" rules (custom regex checks)
149
+ if (check === "regCheck") {
150
+ const isValid = validationRules.regCheck(value, checkValue);
151
+ if (!isValid) {
152
+ return message;
153
+ }
154
+ }
155
+
156
+ // Check for "minValue"
157
+ if (check === 'minValue' && !validationRules.minValue(value, checkValue)) {
158
+ return message;
159
+ }
160
+
161
+ // Check for "maxValue"
162
+ if (check === 'maxValue' && !validationRules.maxValue(value, checkValue)) {
163
+ return message;
164
+ }
165
+
166
+ // Check for "minValueDate"
167
+ if (check === 'minValueDate' && !validationRules.minValueDate(value, checkValue)) {
168
+ return message;
169
+ }
170
+
171
+ // Check for "maxValueDate"
172
+ if (check === 'maxValueDate' && !validationRules.maxValueDate(value, checkValue)) {
173
+ return message;
174
+ }
175
+
176
+ // Check for "minLength"
177
+ if (check === 'minLength' && !validationRules.minLength(value, checkValue)) {
178
+ return message;
179
+ }
180
+
181
+ }
182
+
183
+ return null;
184
+ }
185
+
186
+ // Helper function to validate IBAN
187
+ function validateIBANChecksum(iban) {
188
+ // Move the first four characters to the end
189
+ const rearranged = iban.slice(4) + iban.slice(0, 4);
190
+
191
+ // Replace letters with numbers (A=10, B=11, ..., Z=35)
192
+ const numericIBAN = rearranged.replace(/[A-Z]/g, (char) => char.charCodeAt(0) - 55);
193
+
194
+ // Perform modulo 97 operation
195
+ let remainder = numericIBAN;
196
+ while (remainder.length > 2) {
197
+ const chunk = remainder.slice(0, 9); // Process in chunks of up to 9 digits
198
+ remainder = (parseInt(chunk, 10) % 97) + remainder.slice(chunk.length);
199
+ }
200
+
201
+ return parseInt(remainder, 10) % 97 === 1;
202
+ }
203
+
204
+ // Helper function to normalize numbers
205
+ function normalizeNumber(value) {
206
+ if (typeof value !== 'string') {
207
+ return NaN; // Ensure the input is a string
208
+ }
209
+ // Remove thousands separators (.)
210
+ const withoutThousandsSeparator = value.replace(/\./g, '');
211
+ // Replace the decimal separator (,) with a dot (.)
212
+ const normalizedValue = withoutThousandsSeparator.replace(',', '.');
213
+ return parseFloat(normalizedValue); // Convert to a number
214
+ }
215
+
216
+
217
+ function parseDate(value) {
218
+ // Check for ISO format (yyyy-mm-dd)
219
+ if (/^\d{4}-\d{1,2}-\d{1,2}$/.test(value)) {
220
+ const [year, month, day] = value.split('-').map(Number);
221
+ const parsedDate = new Date(year, month - 1, day); // JavaScript months are 0-based
222
+ if (
223
+ parsedDate.getFullYear() === year &&
224
+ parsedDate.getMonth() === month - 1 &&
225
+ parsedDate.getDate() === day
226
+ ) {
227
+ return parsedDate;
228
+ }
229
+ }
230
+
231
+ // Check for DMY format (d/m/yyyy)
232
+ if (/^\d{1,2}\/\d{1,2}\/\d{4}$/.test(value)) {
233
+ const [day, month, year] = value.split('/').map(Number);
234
+ const parsedDate = new Date(year, month - 1, day); // JavaScript months are 0-based
235
+ if (
236
+ parsedDate.getFullYear() === year &&
237
+ parsedDate.getMonth() === month - 1 &&
238
+ parsedDate.getDate() === day
239
+ ) {
240
+ return parsedDate;
241
+ }
242
+ }
243
+
244
+ return NaN; // Return NaN if the format is invalid
245
+ }
246
+
247
+ /**
248
+ * 🔹 Recursive function to validate form fields, including conditionally displayed fields.
249
+ * @param {Array} elements - The form elements (including conditional ones)
250
+ * @param {Object} formData - The submitted form data
251
+ * @param {string} pageUrl - Use this when linking error summary with the page instead of the element
252
+ * @returns {Object} validationErrors - The object containing validation errors
253
+ */
254
+ export function validateFormElements(elements, formData, pageUrl) {
255
+ const validationErrors = {};
256
+ elements.forEach(field => {
257
+ const inputElements = ALLOWED_FORM_ELEMENTS;
258
+ //only validate input elements
259
+ if (inputElements.includes(field.element)) {
260
+ const fieldValue = (field.element === "dateInput")
261
+ ? [formData[`${field.params.name}_year`],
262
+ formData[`${field.params.name}_month`],
263
+ formData[`${field.params.name}_day`]]
264
+ .filter(Boolean) // Remove empty values
265
+ .join("-") // Join remaining parts
266
+ : formData[field.params.name] || ""; // Get submitted value
267
+
268
+ //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
269
+ if (["checkboxes", "radios", "select"].includes(field.element) && fieldValue !== "") {
270
+ const valuesToCheck = Array.isArray(fieldValue) ? fieldValue : [fieldValue]; // Ensure it's always an array
271
+ const isMatch = valuesToCheck.every(value =>
272
+ field.params.items.some(item => item.value === value)
273
+ );
274
+
275
+ if (!isMatch) {
276
+ validationErrors[(pageUrl ? pageUrl : "") + field.params.name] = {
277
+ id: field.params.id,
278
+ message: govcyResources.staticResources.text.valueNotOnList,
279
+ pageUrl: pageUrl || "",
280
+ };
281
+ }
282
+ }
283
+
284
+ if (field.validations) {
285
+ // 🔍 Validate the field using all its validation rules
286
+ const errorMessage = validateValue(fieldValue, field.validations);
287
+ if (errorMessage) {
288
+ if (!validationErrors[field.params.name]) {
289
+ validationErrors[(pageUrl ? pageUrl : "") + field.params.name] = {};
290
+ }
291
+ validationErrors[(pageUrl ? pageUrl : "") + field.params.name] = {
292
+ id: field.params.id,
293
+ message: errorMessage,
294
+ pageUrl: pageUrl || "",
295
+ };
296
+ }
297
+ }
298
+
299
+ // 🔹 Handle conditional fields (only validate them if the parent condition is met)
300
+ // Handle conditional elements inside radios
301
+ if (field.element === "radios" && field.params.items) {
302
+ field.params.items.forEach(item => {
303
+ if (item.conditionalElements && fieldValue === item.value) {
304
+ if (Array.isArray(item.conditionalElements)) {
305
+ item.conditionalElements.forEach(conditionalElement => {
306
+
307
+ const conditionalFieldValue = (conditionalElement.element === "dateInput")
308
+ ? [formData[`${conditionalElement.params.name}_year`],
309
+ formData[`${conditionalElement.params.name}_month`],
310
+ formData[`${conditionalElement.params.name}_day`]]
311
+ .filter(Boolean) // Remove empty values
312
+ .join("-") // Join remaining parts
313
+ : formData[conditionalElement.params.name] || ""; // Get submitted value
314
+
315
+ //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`
316
+ if (["checkboxes", "radios", "select"].includes(conditionalElement.element) && conditionalFieldValue !== "") {
317
+ const valuesToCheck = Array.isArray(conditionalFieldValue) ? conditionalFieldValue : [conditionalFieldValue]; // Ensure it's always an array
318
+ const isMatch = valuesToCheck.every(value =>
319
+ conditionalElement.params.items.some(item => item.value === value)
320
+ );
321
+
322
+ if (!isMatch) {
323
+ validationErrors[(pageUrl ? pageUrl : "") + conditionalElement.params.name] = {
324
+ id: conditionalElement.params.id,
325
+ message: govcyResources.staticResources.text.valueNotOnList,
326
+ pageUrl: pageUrl || "",
327
+ };
328
+ }
329
+ }
330
+ //if conditional element has validations
331
+ if (conditionalElement.validations) {
332
+ const errorMessage = validateValue(conditionalFieldValue, conditionalElement.validations);
333
+ if (errorMessage) {
334
+ if (!validationErrors[conditionalElement.params.name]) {
335
+ validationErrors[(pageUrl ? pageUrl : "") + conditionalElement.params.name] = {};
336
+ }
337
+ validationErrors[(pageUrl ? pageUrl : "") + conditionalElement.params.name] = {
338
+ id: conditionalElement.params.id,
339
+ message: errorMessage,
340
+ pageUrl: pageUrl || "",
341
+ };
342
+ }
343
+ }
344
+ });
345
+ }
346
+ }
347
+ });
348
+ }
349
+ }
350
+ });
351
+ return validationErrors;
352
+ }