@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,530 @@
1
+
2
+ import * as govcyResources from "../resources/govcyResources.mjs";
3
+ import * as dataLayer from "./govcyDataLayer.mjs";
4
+ import { DSFEmailRenderer } from '@gov-cy/dsf-email-templates';
5
+ import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
6
+
7
+ /**
8
+ * Prepares the submission data for the service, including raw data, print-friendly data, and renderer data.
9
+ *
10
+ * @param {object} req The request object containing session data
11
+ * @param {string} siteId The site ID
12
+ * @param {object} service The service object containing site and page data
13
+ * @returns {object} The submission data object containing raw data, print-friendly data, and renderer data
14
+ */
15
+ export function prepareSubmissionData(req, siteId, service) {
16
+ // Get the raw data from the session store
17
+ const rawData = dataLayer.getSiteInputData(req.session, siteId);
18
+
19
+ // Get the print-friendly data from the session store
20
+ const printFriendlyData = preparePrintFriendlyData(req, siteId, service);
21
+
22
+ // Get the renderer data from the session store
23
+ const reviewSummaryList = generateReviewSummary(printFriendlyData, req, siteId, false);
24
+
25
+ // Prepare the submission data object
26
+ return {
27
+ submission_username: dataLayer.getUser(req.session).name,
28
+ submission_email: dataLayer.getUser(req.session).email,
29
+ submission_data: rawData, // Raw data as submitted by the user in each page
30
+ submission_data_version: service.site?.submission_data_version || "", // The submission data version
31
+ print_friendly_data: printFriendlyData, // Print-friendly data
32
+ renderer_data: reviewSummaryList, // Renderer data of the summary list
33
+ renderer_version: service.site?.renderer_version || "", // The renderer version
34
+ design_systems_version: service.site?.design_systems_version || "", // The design systems version
35
+ service: { // Service info
36
+ id: service.site.id, // Service ID
37
+ title: service.site.title // Service title multilingual object
38
+ },
39
+ referenceNumber: "", // Reference number
40
+ // timestamp: new Date().toISOString(), // Timestamp `new Date().toISOString();`
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Prepares the submission data for the API, stringifying all relevant fields.
46
+ *
47
+ * @param {object} data data prepared by `prepareSubmissionData`
48
+ * @returns {object} The API-ready submission data object with all fields as strings
49
+ */
50
+ export function prepareSubmissionDataAPI(data) {
51
+
52
+ return {
53
+ submission_username: String(data.submission_username ?? ""),
54
+ submission_email: String(data.submission_email ?? ""),
55
+ submission_data: JSON.stringify(data.submission_data ?? {}),
56
+ submission_data_version: String(data.submission_data_version ?? ""),
57
+ print_friendly_data: JSON.stringify(data.print_friendly_data ?? []),
58
+ renderer_data: JSON.stringify(data.renderer_data ?? {}),
59
+ renderer_version: String(data.renderer_version ?? ""),
60
+ design_systems_version: String(data.design_systems_version ?? ""),
61
+ service: JSON.stringify(data.service ?? {})
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Prepares the print-friendly data for the service, including page data and field labels.
67
+ *
68
+ * @param {object} req The request object containing session data
69
+ * @param {string} siteId The site ID
70
+ * @param {object} service The service object containing site and page data
71
+ * @returns The print-friendly data for the service, including page data and field labels.
72
+ */
73
+ export function preparePrintFriendlyData(req, siteId, service) {
74
+ const submissionData = [];
75
+
76
+ const allowedElements = ALLOWED_FORM_ELEMENTS;
77
+
78
+ /**
79
+ * Helper function to retrieve date raw input.
80
+ *
81
+ * @param {string} pageUrl The page URL
82
+ * @param {string} name The name of the form element
83
+ * @returns {string} The raw date input in ISO format (YYYY-MM-DD) or an empty string if not found
84
+ */
85
+ function getDateInputISO(pageUrl, name) {
86
+ const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day`);
87
+ const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month`);
88
+ const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year`);
89
+ if (!day || !month || !year) return "";
90
+
91
+ // Pad day and month with leading zero if needed
92
+ const paddedDay = String(day).padStart(2, "0");
93
+ const paddedMonth = String(month).padStart(2, "0");
94
+
95
+ return `${year}-${paddedMonth}-${paddedDay}`; // ISO format: YYYY-MM-DD
96
+ }
97
+
98
+ /**
99
+ * Helper function to retrieve date input in DMY format.
100
+ *
101
+ * @param {string} pageUrl The page URL
102
+ * @param {string} name The name of the form element
103
+ * @returns {string} The raw date input in DMY format (DD/MM/YYYY) or an empty string if not found
104
+ */
105
+ function getDateInputDMY(pageUrl, name) {
106
+ const day = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_day`);
107
+ const month = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_month`);
108
+ const year = dataLayer.getFormDataValue(req.session, siteId, pageUrl, `${name}_year`);
109
+ if (!day || !month || !year) return "";
110
+ return `${day}/${month}/${year}`; // EU format: DD/MM/YYYY
111
+ }
112
+
113
+ /**
114
+ * Helper function to create a field object.
115
+ *
116
+ * @param {object} formElement The form element object
117
+ * @param {string} value The value of the form element
118
+ * @param {object} valueLabel The label of the form element
119
+ * @returns {object} The field object containing id, label, value, and valueLabel
120
+ */
121
+ function createFieldObject(formElement, value, valueLabel) {
122
+ return {
123
+ id: formElement.params?.id || "",
124
+ name: formElement.params?.name || "",
125
+ label: formElement.params.label
126
+ || formElement.params.legend
127
+ || govcyResources.getSameMultilingualObject(service.site.languages, formElement.params.name),
128
+ value: value,
129
+ valueLabel: valueLabel
130
+ };
131
+ }
132
+
133
+ /**
134
+ * Helper function to retrieve the value of a form element from the session.
135
+ *
136
+ * @param {object} formElement The form element object
137
+ * @param {string} pageUrl The page URL
138
+ * @returns {string} The value of the form element from the session or an empty string if not found
139
+ */
140
+ function getValue(formElement, pageUrl) {
141
+ // handle raw value
142
+ let value = ""
143
+ if (formElement.element === "dateInput") {
144
+ value = getDateInputISO(pageUrl, formElement.params.name);
145
+ } else {
146
+ value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
147
+ }
148
+ return value;
149
+ }
150
+
151
+ /**
152
+ * Helper function to get the label of a form element based on its value and type.
153
+ *
154
+ * @param {object} formElement The form element object
155
+ * @param {string} value The value of the form element
156
+ * @param {string} pageUrl The page URL
157
+ * @returns {object} The label of the form element based on the value and element type
158
+ */
159
+ function getValueLabel(formElement, value, pageUrl) {
160
+ //handle checkboxes label
161
+ if (formElement.element === "checkboxes") {
162
+ if (Array.isArray(value)) {
163
+ // loop through each value and find the corresponding item
164
+ return value.map(v => {
165
+ // find the item
166
+ const item = formElement.params.items.find(i => i.value === v);
167
+ return item?.text || govcyResources.getSameMultilingualObject(service.site.languages, "");
168
+ });
169
+ } else if (typeof value === "string") {
170
+ const matchedItem = formElement.params.items.find(item => item.value === value);
171
+ if (matchedItem) {
172
+ return matchedItem.text;
173
+ } else {
174
+ return govcyResources.getSameMultilingualObject(service.site.languages, "")
175
+ }
176
+ }
177
+ }
178
+
179
+ // handle radios and select labels
180
+ if (formElement.element === "radios" || formElement.element === "select") {
181
+ const item = formElement.params.items.find(i => i.value === value);
182
+ return item?.text || govcyResources.getSameMultilingualObject(service.site.languages, "");
183
+ }
184
+
185
+ // handle dateInput
186
+ if (formElement.element === "dateInput") {
187
+ const formattedDate = getDateInputDMY(pageUrl, formElement.params.name);
188
+ return govcyResources.getSameMultilingualObject(service.site.languages, formattedDate);
189
+ }
190
+
191
+ // textInput, textArea, etc.
192
+ return govcyResources.getSameMultilingualObject(service.site.languages, value);
193
+ }
194
+
195
+ // loop through each page in the service
196
+ // and extract the form data from the session
197
+ for (const page of service.pages) {
198
+ const fields = [];
199
+ // const currentPageUrl = page.pageData.url;
200
+
201
+ // find the form element in the page template
202
+ for (const section of page.pageTemplate.sections || []) {
203
+ for (const element of section.elements || []) {
204
+ if (element.element !== "form") continue;
205
+
206
+ // loop through each form element and get the data from the session
207
+ for (const formElement of element.params.elements || []) {
208
+ if (!allowedElements.includes(formElement.element)) continue;
209
+
210
+ // handle raw value
211
+ let rawValue = getValue(formElement, page.pageData.url);
212
+
213
+ //create the field object and push it to the fields array
214
+ // value of the field is handled by getValueLabel function
215
+ const field = createFieldObject(formElement, rawValue, getValueLabel(formElement, rawValue, page.pageData.url));
216
+ fields.push(field);
217
+
218
+ // Handle conditional elements (only for radios for now)
219
+ if (formElement.element === "radios") {
220
+ //find the selected radio based on the raw value
221
+ const selectedRadio = formElement.params.items.find(i => i.value === rawValue);
222
+ //check if the selected radio has conditional elements
223
+ if (selectedRadio?.conditionalElements) {
224
+ //loop through each conditional element and get the data
225
+ for (const condEl of selectedRadio.conditionalElements) {
226
+ if (!allowedElements.includes(condEl.element)) continue;
227
+
228
+ // handle raw value
229
+ let condValue = getValue(condEl, page.pageData.url);
230
+
231
+ //create the field object and push it to the fields array
232
+ // value of the field is handled by getValueLabel function
233
+ const field = createFieldObject(condEl, condValue, getValueLabel(condEl, condValue, page.pageData.url));
234
+ fields.push(field);
235
+ }
236
+ }
237
+ }
238
+ }
239
+ }
240
+ }
241
+
242
+ if (fields.length > 0) {
243
+ submissionData.push({
244
+ pageUrl: page.pageData.url,
245
+ pageTitle: page.pageData.title,
246
+ fields
247
+ });
248
+ }
249
+ }
250
+
251
+ return submissionData ;
252
+ }
253
+
254
+ //------------------------------- Helper Functions -------------------------------//
255
+
256
+ /**
257
+ * Helper function to get the item value of checkboxes based on the selected value.
258
+ * @param {object} valueLabel the value
259
+ * @param {string} lang the language
260
+ * @returns {string} the item value of checkboxes
261
+ */
262
+ function getSubmissionValueLabelString(valueLabel, lang, fallbackLang = "en") {
263
+ if (!valueLabel) return "";
264
+
265
+ // Helper to get the desired language string or fallback
266
+ const getText = (obj) =>
267
+ obj?.[lang]?.trim() || obj?.[fallbackLang]?.trim() || "";
268
+
269
+ // Case 1: Array of multilingual objects
270
+ if (Array.isArray(valueLabel)) {
271
+ return valueLabel
272
+ .map(getText) // get lang/fallback string for each item
273
+ .filter(Boolean) // remove empty strings
274
+ .join(", "); // join with comma
275
+ }
276
+
277
+ // Case 2: Single multilingual object
278
+ if (typeof valueLabel === "object") {
279
+ return getText(valueLabel);
280
+ }
281
+
282
+ // Graceful fallback
283
+ return "";
284
+ }
285
+
286
+ //------------------------------- Review Summary -------------------------------//
287
+ /**
288
+ * Generates a review summary for the submission data, ready to be rendered.
289
+ *
290
+ * @param {object} submissionData The submission data object containing page data and fields
291
+ * @param {object} req The request object containing global language and session data
292
+ * @param {string} siteId The site ID
293
+ * @param {boolean} showChangeLinks Flag to show change links or not
294
+ * @returns {object} The review summary to be rendered by the renderer
295
+ */
296
+ export function generateReviewSummary(submissionData, req, siteId, showChangeLinks = true) {
297
+ // Base summary list structure
298
+ let summaryList = { element: "summaryList", params: { items: [] } };
299
+
300
+ /**
301
+ * Helper function to create a summary list item.
302
+ * @param {object} key the key of multilingual object
303
+ * @param {string} value the value
304
+ * @returns {object} the summary list item
305
+ */
306
+ function createSummaryListItem(key, value) {
307
+ return {
308
+ "key": key,
309
+ "value": [
310
+ {
311
+ "element": "textElement",
312
+ "params": {
313
+ "text": { "en": value, "el": value, "tr": value },
314
+ "type": "span"
315
+ }
316
+ }
317
+ ]
318
+ };
319
+ }
320
+
321
+
322
+
323
+
324
+ // Loop through each page in the submission data
325
+ for (const page of submissionData) {
326
+ // Get the page URL, title, and fields
327
+ const { pageUrl, pageTitle, fields } = page;
328
+
329
+
330
+ let summaryListInner = { element: "summaryList", params: { items: [] } };
331
+
332
+ // loop through each field and add it to the summary entry
333
+ for (const field of fields) {
334
+ const label = field.label;
335
+ const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
336
+ // add the field to the summary entry
337
+ summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
338
+ }
339
+
340
+ // Add inner summary list to the main summary list
341
+ let outerSummaryList = {
342
+ "key": pageTitle,
343
+ "value": [summaryListInner],
344
+ "actions": [ //add change link
345
+ {
346
+ text: govcyResources.staticResources.text.change,
347
+ classes: govcyResources.staticResources.other.noPrintClass,
348
+ href: govcyResources.constructPageUrl(siteId, pageUrl, "review"),
349
+ visuallyHiddenText: pageTitle
350
+ }
351
+ ]
352
+ };
353
+
354
+ // If showChangeLinks is false, remove the change link
355
+ if (!showChangeLinks) {
356
+ delete outerSummaryList.actions;
357
+ }
358
+
359
+ //push to the main summary list
360
+ summaryList.params.items.push(outerSummaryList);
361
+
362
+ }
363
+
364
+ return summaryList;
365
+ }
366
+
367
+
368
+ //------------------------------- Email Generation -------------------------------//
369
+ /**
370
+ * Generates an email HTML body for the submission data, ready to be sent.
371
+ *
372
+ * @param {object} service The service object
373
+ * @param {object} submissionData The submission data object containing page data and fields
374
+ * @param {string} submissionId The submission id
375
+ * @param {object} req The request object containing global language and session data
376
+ * @returns {string} The email HTML body
377
+ */
378
+ export function generateSubmitEmail(service, submissionData, submissionId, req) {
379
+ let body = [];
380
+
381
+ //check if there is submission Id
382
+ if (submissionId) {
383
+ // Add success message to the body
384
+ body.push(
385
+ {
386
+ component: "bodySuccess",
387
+ params: {
388
+ title: govcyResources.getLocalizeContent(govcyResources.staticResources.text.submissionSuccessTitle, req.globalLang),
389
+ body: `${govcyResources.getLocalizeContent(govcyResources.staticResources.text.yourSubmissionId, req.globalLang)} ${submissionId}`
390
+ }
391
+ }
392
+ );
393
+ }
394
+
395
+ // Add data title to the body
396
+ body.push(
397
+ {
398
+ component: "bodyParagraph",
399
+ body: govcyResources.getLocalizeContent(govcyResources.staticResources.text.theDataFromYourRequest, req.globalLang)
400
+ }
401
+ );
402
+
403
+ // For each page in the submission data
404
+ for (const page of submissionData) {
405
+ // Get the page URL, title, and fields
406
+ const { pageUrl, pageTitle, fields } = page;
407
+
408
+ // Add data title to the body
409
+ body.push(
410
+ {
411
+ component: "bodyHeading",
412
+ params: {"headingLevel":2},
413
+ body: govcyResources.getLocalizeContent(pageTitle, req.globalLang)
414
+ }
415
+ );
416
+
417
+ let dataUl = [];
418
+ // loop through each field and add it to the summary entry
419
+ for (const field of fields) {
420
+ const label = govcyResources.getLocalizeContent(field.label, req.globalLang);
421
+ const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
422
+ dataUl.push({key: label, value: valueLabel});
423
+ }
424
+ // add data to the body
425
+ body.push(
426
+ {
427
+ component: "bodyKeyValue",
428
+ params: {type:"ul", items: dataUl},
429
+ });
430
+
431
+ }
432
+
433
+ let emailObject = govcyResources.getEmailObject(
434
+ service.site.title,
435
+ govcyResources.staticResources.text.emailSubmissionPreHeader,
436
+ service.site.title,
437
+ dataLayer.getUser(req.session).name,
438
+ body,
439
+ service.site.title,
440
+ req.globalLang
441
+ )
442
+
443
+ // Create an instance of DSFEmailRenderer
444
+ const emailRenderer = new DSFEmailRenderer();
445
+ return emailRenderer.renderFromJson(emailObject);
446
+ }
447
+
448
+
449
+
450
+ /*
451
+ {
452
+ "bank-details": {
453
+ "formData": {
454
+ "AccountName": "asd",
455
+ "Iban": "CY12 0020 0123 0000 0001 2345 6789",
456
+ "Swift": "BANKCY2NXXX",
457
+ "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
458
+ }
459
+ },
460
+ "answer-bank-boc": {
461
+ "formData": {
462
+ "Objection": "Object",
463
+ "country": "Azerbaijan",
464
+ "ObjectionReason": "ObjectionReasonCode1",
465
+ "ObjectionExplanation": "asdsa",
466
+ "DepositsBOCAttachment": "",
467
+ "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
468
+ }
469
+ },
470
+ "bank-settlement": {
471
+ "formData": {
472
+ "ReceiveSettlementExplanation": "",
473
+ "ReceiveSettlementDate_day": "",
474
+ "ReceiveSettlementDate_month": "",
475
+ "ReceiveSettlementDate_year": "",
476
+ "ReceiveSettlement": "no",
477
+ "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
478
+ }
479
+ }
480
+ }
481
+
482
+
483
+
484
+ [
485
+ {
486
+ pageUrl: "personal-details",
487
+ pageTitle: { en: "Personal data", el: "Προσωπικά στοιχεία" }, // from pageData.title in correct language
488
+ fields: [
489
+ [
490
+ {
491
+ id: "firstName",
492
+ label: { en: "First Name", el: "Όνομα" },
493
+ value: "John", // The actual user input value
494
+ valueLabel: { en: "John", el: "John" } // Same label as the value for text inputs
495
+ },
496
+ {
497
+ id: "lastName",
498
+ label: { en: "Last Name", el: "Επίθετο" },
499
+ value: "Doe", // The actual user input value
500
+ valueLabel: { en: "Doe", el: "Doe" } // Same label as the value for text inputs
501
+ },
502
+ {
503
+ id: "gender",
504
+ label: { en: "Gender", el: "Φύλο" },
505
+ value: "m", // The actual value ("male")
506
+ valueLabel: { en: "Male", el: "Άντρας" } // The corresponding label for "male"
507
+ },
508
+ {
509
+ id: "languages",
510
+ label: { en: "Languages", el: "Γλώσσες" },
511
+ value: ["en", "el"], // The selected values ["en", "el"]
512
+ valueLabel: [
513
+ { en: "English", el: "Αγγλικά" }, // Labels corresponding to "en" and "el"
514
+ { en: "Greek", el: "Ελληνικά" }
515
+ ]
516
+ },
517
+ {
518
+ id: "birthDate",
519
+ label: { en: "Birth Date", el: "Ημερομηνία Γέννησης" },
520
+ value: "1990-01-13", // The actual value based on user input
521
+ valueLabel: "13/1/1990" // Date inputs label will be conveted to D/M/YYYY
522
+ }
523
+ ]
524
+ },
525
+ ...
526
+ ]
527
+
528
+
529
+
530
+ */
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Helper function to handle errors in middleware.
3
+ * Creates an error object and passes it to the next middleware.
4
+ *
5
+ * @param {string} message - The error message.
6
+ * @param {number} status - The HTTP status code.
7
+ * @param {function} next - The Express `next` function.
8
+ */
9
+ export function handleMiddlewareError(message, status, next) {
10
+ const error = new Error(message);
11
+ error.status = status;
12
+ return next(error);
13
+ }