@gov-cy/govcy-express-services 1.0.0-alpha.2 → 1.0.0-alpha.20

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 (32) hide show
  1. package/README.md +1051 -83
  2. package/package.json +10 -3
  3. package/src/auth/cyLoginAuth.mjs +2 -1
  4. package/src/index.mjs +20 -1
  5. package/src/middleware/cyLoginAuth.mjs +11 -1
  6. package/src/middleware/govcyCsrf.mjs +15 -1
  7. package/src/middleware/govcyFileDeleteHandler.mjs +320 -0
  8. package/src/middleware/govcyFileUpload.mjs +36 -0
  9. package/src/middleware/govcyFileViewHandler.mjs +161 -0
  10. package/src/middleware/govcyFormsPostHandler.mjs +1 -1
  11. package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
  12. package/src/middleware/govcyPDFRender.mjs +3 -1
  13. package/src/middleware/govcyPageHandler.mjs +3 -3
  14. package/src/middleware/govcyPageRender.mjs +10 -0
  15. package/src/middleware/govcyReviewPageHandler.mjs +4 -1
  16. package/src/middleware/govcyReviewPostHandler.mjs +1 -1
  17. package/src/middleware/govcySuccessPageHandler.mjs +2 -3
  18. package/src/public/js/govcyFiles.js +299 -0
  19. package/src/public/js/govcyForms.js +19 -8
  20. package/src/resources/govcyResources.mjs +85 -4
  21. package/src/utils/govcyApiDetection.mjs +17 -0
  22. package/src/utils/govcyApiRequest.mjs +30 -5
  23. package/src/utils/govcyApiResponse.mjs +31 -0
  24. package/src/utils/govcyConstants.mjs +5 -1
  25. package/src/utils/govcyDataLayer.mjs +211 -11
  26. package/src/utils/govcyExpressions.mjs +1 -1
  27. package/src/utils/govcyFormHandling.mjs +81 -5
  28. package/src/utils/govcyHandleFiles.mjs +307 -0
  29. package/src/utils/govcyLoadSubmissionDataAPIs.mjs +10 -3
  30. package/src/utils/govcySubmitData.mjs +186 -106
  31. package/src/utils/govcyTempSave.mjs +2 -1
  32. package/src/utils/govcyValidator.mjs +7 -0
@@ -17,7 +17,7 @@ import { logger } from "./govcyLogger.mjs";
17
17
  export function prepareSubmissionData(req, siteId, service) {
18
18
  // Get the raw data from the session store
19
19
  // const rawData = dataLayer.getSiteInputData(req.session, siteId);
20
-
20
+
21
21
  // ----- Conditional logic comes here
22
22
  // Filter site input data based on active pages only
23
23
  // const rawData = {};
@@ -32,7 +32,7 @@ export function prepareSubmissionData(req, siteId, service) {
32
32
  // }
33
33
  // }
34
34
 
35
- // ----- consistent data model for submission_data (CONFIG-BASED)
35
+ // ----- consistent data model for submissionData (CONFIG-BASED)
36
36
  const submissionData = {};
37
37
 
38
38
  // Loop through every page in the service definition
@@ -48,7 +48,8 @@ export function prepareSubmissionData(req, siteId, service) {
48
48
 
49
49
  if (!formElement) continue; // ⛔ Skip pages without a <form> element
50
50
 
51
- submissionData[pageUrl] = { formData: {} }; // ✅ Now initialize only if a form is present
51
+ // submissionData[pageUrl] = { formData: {} }; // ✅ Now initialize only if a form is present
52
+ submissionData[pageUrl] = {}; // ✅ Now initialize only if a form is present
52
53
 
53
54
  // Traverse the form elements inside the form
54
55
  for (const element of formElement.params.elements || []) {
@@ -64,8 +65,17 @@ export function prepareSubmissionData(req, siteId, service) {
64
65
  const value = getValue(element, pageUrl, req, siteId) ?? "";
65
66
 
66
67
  // Store in submissionData
67
- submissionData[pageUrl].formData[elId] = value;
68
-
68
+ // submissionData[pageUrl].formData[elId] = value;
69
+ submissionData[pageUrl][elId] = value;
70
+
71
+ // handle fileInput
72
+ if (elType === "fileInput") {
73
+ // change the name of the key to include "Attachment" at the end but not have the original key
74
+ // submissionData[pageUrl].formData[elId + "Attachment"] = value;
75
+ submissionData[pageUrl][elId + "Attachment"] = value;
76
+ // delete submissionData[pageUrl].formData[elId];
77
+ delete submissionData[pageUrl][elId];
78
+ }
69
79
 
70
80
  // 🔄 If radios with conditionalElements, walk ALL options
71
81
  if (elType === "radios" && Array.isArray(element.params?.items)) {
@@ -84,14 +94,23 @@ export function prepareSubmissionData(req, siteId, service) {
84
94
  const condValue = getValue(condElement, pageUrl, req, siteId) ?? "";
85
95
 
86
96
  // Store even if the field was not visible to user
87
- submissionData[pageUrl].formData[condId] = condValue;
97
+ // submissionData[pageUrl].formData[condId] = condValue;
98
+ submissionData[pageUrl][condId] = condValue;
99
+ // handle fileInput
100
+ if (condType === "fileInput") {
101
+ // change the name of the key to include "Attachment" at the end but not have the original key
102
+ // submissionData[pageUrl].formData[condId + "Attachment"] = condValue;
103
+ submissionData[pageUrl][condId + "Attachment"] = condValue;
104
+ // delete submissionData[pageUrl].formData[condId];
105
+ delete submissionData[pageUrl][condId];
106
+ }
88
107
  }
89
108
  }
90
109
  }
91
110
  }
92
111
  }
93
112
  logger.debug("Submission Data prepared:", submissionData);
94
- // ----- END config-based stable submission_data block
113
+ // ----- END config-based stable submissionData block
95
114
 
96
115
  // Get the print-friendly data from the session store
97
116
  const printFriendlyData = preparePrintFriendlyData(req, siteId, service);
@@ -100,14 +119,14 @@ export function prepareSubmissionData(req, siteId, service) {
100
119
  const reviewSummaryList = generateReviewSummary(printFriendlyData, req, siteId, false);
101
120
  // Prepare the submission data object
102
121
  return {
103
- submission_username: dataLayer.getUser(req.session).name,
104
- submission_email: dataLayer.getUser(req.session).email,
105
- submission_data: submissionData, // Raw data as submitted by the user in each page
106
- submission_data_version: service.site?.submission_data_version || "", // The submission data version
107
- print_friendly_data: printFriendlyData, // Print-friendly data
108
- renderer_data: reviewSummaryList, // Renderer data of the summary list
109
- renderer_version: service.site?.renderer_version || "", // The renderer version
110
- design_systems_version: service.site?.design_systems_version || "", // The design systems version
122
+ submissionUsername: dataLayer.getUser(req.session).name,
123
+ submissionEmail: dataLayer.getUser(req.session).email,
124
+ submissionData: submissionData, // Raw data as submitted by the user in each page
125
+ submissionDataVersion: service.site?.submission_data_version || "", // The submission data version
126
+ printFriendlyData: printFriendlyData, // Print-friendly data
127
+ rendererData: reviewSummaryList, // Renderer data of the summary list
128
+ rendererVersion: service.site?.renderer_version || "", // The renderer version
129
+ designSystemsVersion: service.site?.design_systems_version || "", // The design systems version
111
130
  service: { // Service info
112
131
  id: service.site.id, // Service ID
113
132
  title: service.site.title // Service title multilingual object
@@ -124,16 +143,16 @@ export function prepareSubmissionData(req, siteId, service) {
124
143
  * @returns {object} The API-ready submission data object with all fields as strings
125
144
  */
126
145
  export function prepareSubmissionDataAPI(data) {
127
-
146
+
128
147
  return {
129
- submission_username: String(data.submission_username ?? ""),
130
- submission_email: String(data.submission_email ?? ""),
131
- submission_data: JSON.stringify(data.submission_data ?? {}),
132
- submission_data_version: String(data.submission_data_version ?? ""),
133
- print_friendly_data: JSON.stringify(data.print_friendly_data ?? []),
134
- renderer_data: JSON.stringify(data.renderer_data ?? {}),
135
- renderer_version: String(data.renderer_version ?? ""),
136
- design_systems_version: String(data.design_systems_version ?? ""),
148
+ submissionUsername: String(data.submissionUsername ?? ""),
149
+ submissionEmail: String(data.submissionEmail ?? ""),
150
+ submissionData: JSON.stringify(data.submissionData ?? {}),
151
+ submissionDataVersion: String(data.submissionDataVersion ?? ""),
152
+ printFriendlyData: JSON.stringify(data.printFriendlyData ?? []),
153
+ rendererData: JSON.stringify(data.rendererData ?? {}),
154
+ rendererVersion: String(data.rendererVersion ?? ""),
155
+ designSystemsVersion: String(data.designSystemsVersion ?? ""),
137
156
  service: JSON.stringify(data.service ?? {})
138
157
  };
139
158
  }
@@ -213,7 +232,7 @@ export function preparePrintFriendlyData(req, siteId, service) {
213
232
  }
214
233
  }
215
234
 
216
- return submissionData ;
235
+ return submissionData;
217
236
  }
218
237
 
219
238
  //------------------------------- Helper Functions -------------------------------//
@@ -291,9 +310,26 @@ function getValue(formElement, pageUrl, req, siteId) {
291
310
  let value = ""
292
311
  if (formElement.element === "dateInput") {
293
312
  value = getDateInputISO(pageUrl, formElement.params.name, req, siteId);
313
+ } else if (formElement.element === "fileInput") {
314
+ // unneeded handle of `Attachment` at the end
315
+ // value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name + "Attachment");
316
+ value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
294
317
  } else {
295
318
  value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
296
319
  }
320
+
321
+ // 🔁 Normalize checkboxes: always return an array
322
+ if (formElement.element === "checkboxes") {
323
+ // If no value, return empty array
324
+ if (value == null || value === "") return [];
325
+ // If already an array, return as-is (but strip empties just in case)
326
+ if (Array.isArray(value)) {
327
+ // Strip empties just in case
328
+ return value.filter(v => v != null && v !== "");
329
+ }
330
+ // Else single value, convert to array
331
+ return [String(value)];
332
+ }
297
333
  return value;
298
334
  }
299
335
 
@@ -340,6 +376,17 @@ function getValueLabel(formElement, value, pageUrl, req, siteId, service) {
340
376
  return govcyResources.getSameMultilingualObject(service.site.languages, formattedDate);
341
377
  }
342
378
 
379
+ // handle fileInput
380
+ if (formElement.element === "fileInput") {
381
+ // TODO: Ask Andreas how to handle empty file inputs
382
+ if (value) {
383
+ return govcyResources.staticResources.text.fileUploaded;
384
+ } else {
385
+ return govcyResources.getSameMultilingualObject(service.site.languages, "");
386
+ // return govcyResources.staticResources.text.fileNotUploaded;
387
+ }
388
+ }
389
+
343
390
  // textInput, textArea, etc.
344
391
  return govcyResources.getSameMultilingualObject(service.site.languages, value);
345
392
  }
@@ -410,7 +457,34 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
410
457
  };
411
458
  }
412
459
 
413
-
460
+ /**
461
+ * Helper function to create a summary list item for file links.
462
+ * @param {object} key the key of multilingual object
463
+ * @param {string} value the value
464
+ * @param {string} siteId the site id
465
+ * @param {string} pageUrl the page url
466
+ * @param {string} elementName the element name
467
+ * @returns {object} the summary list item with file link
468
+ */
469
+ function createSummaryListItemFileLink(key, value, siteId, pageUrl, elementName) {
470
+ return {
471
+ "key": key,
472
+ "value": [
473
+ {
474
+ "element": "htmlElement",
475
+ "params": {
476
+ "text": {
477
+ "en": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.en}<span class="govcy-visually-hidden"> ${key?.en || ""}</span></a>`,
478
+ "el": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.el}<span class="govcy-visually-hidden"> ${key?.el || ""}</span></a>`,
479
+ "tr": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.tr}<span class="govcy-visually-hidden"> ${key?.tr || ""}</span></a>`
480
+ }
481
+ }
482
+ }
483
+ ]
484
+ };
485
+ }
486
+
487
+
414
488
 
415
489
 
416
490
  // Loop through each page in the submission data
@@ -425,8 +499,14 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
425
499
  for (const field of fields) {
426
500
  const label = field.label;
427
501
  const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
428
- // add the field to the summary entry
429
- summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
502
+ // --- HACK --- to see if this is a file element
503
+ // check if field.value is an object with `sha256` and `fileId` properties
504
+ if (typeof field.value === "object" && field.value.hasOwnProperty("sha256") && field.value.hasOwnProperty("fileId") && showChangeLinks) {
505
+ summaryListInner.params.items.push(createSummaryListItemFileLink(label, valueLabel, siteId, pageUrl, field.name));
506
+ } else {
507
+ // add the field to the summary entry
508
+ summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
509
+ }
430
510
  }
431
511
 
432
512
  // Add inner summary list to the main summary list
@@ -483,7 +563,7 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
483
563
  }
484
564
  );
485
565
  }
486
-
566
+
487
567
  // Add data title to the body
488
568
  body.push(
489
569
  {
@@ -501,7 +581,7 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
501
581
  body.push(
502
582
  {
503
583
  component: "bodyHeading",
504
- params: {"headingLevel":2},
584
+ params: { "headingLevel": 2 },
505
585
  body: govcyResources.getLocalizeContent(pageTitle, req.globalLang)
506
586
  }
507
587
  );
@@ -511,14 +591,14 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
511
591
  for (const field of fields) {
512
592
  const label = govcyResources.getLocalizeContent(field.label, req.globalLang);
513
593
  const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
514
- dataUl.push({key: label, value: valueLabel});
594
+ dataUl.push({ key: label, value: valueLabel });
515
595
  }
516
596
  // add data to the body
517
597
  body.push(
518
- {
519
- component: "bodyKeyValue",
520
- params: {type:"ul", items: dataUl},
521
- });
598
+ {
599
+ component: "bodyKeyValue",
600
+ params: { type: "ul", items: dataUl },
601
+ });
522
602
 
523
603
  }
524
604
 
@@ -539,84 +619,84 @@ export function generateSubmitEmail(service, submissionData, submissionId, req)
539
619
 
540
620
 
541
621
 
542
- /*
622
+ /*
543
623
  {
544
- "bank-details": {
545
- "formData": {
546
- "AccountName": "asd",
547
- "Iban": "CY12 0020 0123 0000 0001 2345 6789",
548
- "Swift": "BANKCY2NXXX",
549
- "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
550
- }
551
- },
552
- "answer-bank-boc": {
553
- "formData": {
554
- "Objection": "Object",
555
- "country": "Azerbaijan",
556
- "ObjectionReason": "ObjectionReasonCode1",
557
- "ObjectionExplanation": "asdsa",
558
- "DepositsBOCAttachment": "",
559
- "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
560
- }
561
- },
562
- "bank-settlement": {
563
- "formData": {
564
- "ReceiveSettlementExplanation": "",
565
- "ReceiveSettlementDate_day": "",
566
- "ReceiveSettlementDate_month": "",
567
- "ReceiveSettlementDate_year": "",
568
- "ReceiveSettlement": "no",
569
- "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
570
- }
571
- }
624
+ "bank-details": {
625
+ "formData": {
626
+ "AccountName": "asd",
627
+ "Iban": "CY12 0020 0123 0000 0001 2345 6789",
628
+ "Swift": "BANKCY2NXXX",
629
+ "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
630
+ }
631
+ },
632
+ "answer-bank-boc": {
633
+ "formData": {
634
+ "Objection": "Object",
635
+ "country": "Azerbaijan",
636
+ "ObjectionReason": "ObjectionReasonCode1",
637
+ "ObjectionExplanation": "asdsa",
638
+ "DepositsBOCAttachment": "",
639
+ "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
640
+ }
641
+ },
642
+ "bank-settlement": {
643
+ "formData": {
644
+ "ReceiveSettlementExplanation": "",
645
+ "ReceiveSettlementDate_day": "",
646
+ "ReceiveSettlementDate_month": "",
647
+ "ReceiveSettlementDate_year": "",
648
+ "ReceiveSettlement": "no",
649
+ "_csrf": "sjknv79rxjgv0uggo0d5312vzgz37jsh"
650
+ }
651
+ }
572
652
  }
573
653
 
574
654
 
575
655
 
576
656
  [
577
- {
578
- pageUrl: "personal-details",
579
- pageTitle: { en: "Personal data", el: "Προσωπικά στοιχεία" }, // from pageData.title in correct language
580
- fields: [
581
- [
582
- {
583
- id: "firstName",
584
- label: { en: "First Name", el: "Όνομα" },
585
- value: "John", // The actual user input value
586
- valueLabel: { en: "John", el: "John" } // Same label as the value for text inputs
587
- },
588
- {
589
- id: "lastName",
590
- label: { en: "Last Name", el: "Επίθετο" },
591
- value: "Doe", // The actual user input value
592
- valueLabel: { en: "Doe", el: "Doe" } // Same label as the value for text inputs
593
- },
594
- {
595
- id: "gender",
596
- label: { en: "Gender", el: "Φύλο" },
597
- value: "m", // The actual value ("male")
598
- valueLabel: { en: "Male", el: "Άντρας" } // The corresponding label for "male"
599
- },
600
- {
601
- id: "languages",
602
- label: { en: "Languages", el: "Γλώσσες" },
603
- value: ["en", "el"], // The selected values ["en", "el"]
604
- valueLabel: [
605
- { en: "English", el: "Αγγλικά" }, // Labels corresponding to "en" and "el"
606
- { en: "Greek", el: "Ελληνικά" }
607
- ]
608
- },
609
- {
610
- id: "birthDate",
611
- label: { en: "Birth Date", el: "Ημερομηνία Γέννησης" },
612
- value: "1990-01-13", // The actual value based on user input
613
- valueLabel: "13/1/1990" // Date inputs label will be conveted to D/M/YYYY
614
- }
615
- ]
616
- },
617
- ...
657
+ {
658
+ pageUrl: "personal-details",
659
+ pageTitle: { en: "Personal data", el: "Προσωπικά στοιχεία" }, // from pageData.title in correct language
660
+ fields: [
661
+ [
662
+ {
663
+ id: "firstName",
664
+ label: { en: "First Name", el: "Όνομα" },
665
+ value: "John", // The actual user input value
666
+ valueLabel: { en: "John", el: "John" } // Same label as the value for text inputs
667
+ },
668
+ {
669
+ id: "lastName",
670
+ label: { en: "Last Name", el: "Επίθετο" },
671
+ value: "Doe", // The actual user input value
672
+ valueLabel: { en: "Doe", el: "Doe" } // Same label as the value for text inputs
673
+ },
674
+ {
675
+ id: "gender",
676
+ label: { en: "Gender", el: "Φύλο" },
677
+ value: "m", // The actual value ("male")
678
+ valueLabel: { en: "Male", el: "Άντρας" } // The corresponding label for "male"
679
+ },
680
+ {
681
+ id: "languages",
682
+ label: { en: "Languages", el: "Γλώσσες" },
683
+ value: ["en", "el"], // The selected values ["en", "el"]
684
+ valueLabel: [
685
+ { en: "English", el: "Αγγλικά" }, // Labels corresponding to "en" and "el"
686
+ { en: "Greek", el: "Ελληνικά" }
687
+ ]
688
+ },
689
+ {
690
+ id: "birthDate",
691
+ label: { en: "Birth Date", el: "Ημερομηνία Γέννησης" },
692
+ value: "1990-01-13", // The actual value based on user input
693
+ valueLabel: "13/1/1990" // Date inputs label will be conveted to D/M/YYYY
694
+ }
695
+ ]
696
+ },
697
+ ...
618
698
  ]
619
699
 
620
700
 
621
701
 
622
- */
702
+ */
@@ -27,7 +27,8 @@ export async function tempSaveIfConfigured(store, service, siteId) {
27
27
  const inputData = dataLayer.getSiteInputData(store, siteId) || {};
28
28
  const tempPayload = {
29
29
  // mirror final submission format: send stringified JSON
30
- submission_data: JSON.stringify(inputData)
30
+ // submission_data: JSON.stringify(inputData),
31
+ submissionData: JSON.stringify(inputData)
31
32
  };
32
33
 
33
34
  if (!url || !clientKey) {
@@ -263,6 +263,9 @@ export function validateFormElements(elements, formData, pageUrl) {
263
263
  formData[`${field.params.name}_day`]]
264
264
  .filter(Boolean) // Remove empty values
265
265
  .join("-") // Join remaining parts
266
+ // unneeded handle of `Attachment` at the end
267
+ // : (field.element === "fileInput") // Handle fileInput
268
+ // ? formData[`${field.params.name}Attachment`] || ""
266
269
  : formData[field.params.name] || ""; // Get submitted value
267
270
 
268
271
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
@@ -310,6 +313,10 @@ export function validateFormElements(elements, formData, pageUrl) {
310
313
  formData[`${conditionalElement.params.name}_day`]]
311
314
  .filter(Boolean) // Remove empty values
312
315
  .join("-") // Join remaining parts
316
+ : (conditionalElement.element === "fileInput") // Handle fileInput
317
+ // unneeded handle of `Attachment` at the end
318
+ // ? formData[`${conditionalElement.params.name}Attachment`] || ""
319
+ ? formData[`${conditionalElement.params.name}`] || ""
313
320
  : formData[conditionalElement.params.name] || ""; // Get submitted value
314
321
 
315
322
  //Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`