@gov-cy/govcy-express-services 0.2.16 β 1.0.0-alpha.10
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 +205 -4
- package/package.json +4 -2
- package/src/index.mjs +24 -4
- package/src/middleware/cyLoginAuth.mjs +8 -0
- package/src/middleware/govcyCsrf.mjs +15 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +238 -0
- package/src/middleware/govcyFileUpload.mjs +36 -0
- package/src/middleware/govcyFileViewHandler.mjs +161 -0
- package/src/middleware/govcyFormsPostHandler.mjs +8 -2
- package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
- package/src/middleware/govcyLoadSubmissionData.mjs +22 -0
- package/src/middleware/govcyPageHandler.mjs +5 -1
- package/src/public/js/govcyFiles.js +197 -0
- package/src/public/js/govcyForms.js +19 -8
- package/src/resources/govcyResources.mjs +69 -3
- package/src/utils/govcyApiDetection.mjs +17 -0
- package/src/utils/govcyApiRequest.mjs +30 -5
- package/src/utils/govcyApiResponse.mjs +31 -0
- package/src/utils/govcyConstants.mjs +5 -1
- package/src/utils/govcyDataLayer.mjs +71 -1
- package/src/utils/govcyExpressions.mjs +1 -1
- package/src/utils/govcyFormHandling.mjs +81 -5
- package/src/utils/govcyHandleFiles.mjs +307 -0
- package/src/utils/govcyLoadSubmissionDataAPIs.mjs +142 -0
- package/src/utils/govcySubmitData.mjs +62 -2
- package/src/utils/govcyTempSave.mjs +71 -0
- package/src/utils/govcyValidator.mjs +7 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { govcyApiRequest } from "./govcyApiRequest.mjs";
|
|
2
|
+
import { logger } from "./govcyLogger.mjs";
|
|
3
|
+
import { getEnvVariable, getEnvVariableBool } from "./govcyEnvVariables.mjs";
|
|
4
|
+
import { handleMiddlewareError } from "./govcyUtils.mjs";
|
|
5
|
+
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load submission data from configured APIs and store it in the session.
|
|
9
|
+
* @param {object} store The session store
|
|
10
|
+
* @param {object} service The service configuration
|
|
11
|
+
* @param {string} siteId The site id
|
|
12
|
+
* @param {function} next The next middleware function
|
|
13
|
+
*/
|
|
14
|
+
export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next) {
|
|
15
|
+
try {
|
|
16
|
+
|
|
17
|
+
// Get the API endpoints
|
|
18
|
+
const getCfg = service?.site?.submissionGetAPIEndpoint;
|
|
19
|
+
const putCfg = service?.site?.submissionPutAPIEndpoint;
|
|
20
|
+
|
|
21
|
+
//If siteLoadData already exists, skip the API call
|
|
22
|
+
const siteLoadData = dataLayer.getSiteLoadData(store, siteId);
|
|
23
|
+
if (siteLoadData && Object.keys(siteLoadData).length > 0) {
|
|
24
|
+
// Data exists, skip API call
|
|
25
|
+
logger.debug("Load data already exists for site:", siteId);
|
|
26
|
+
return next();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Only continue if both endpoints and required fields are defined
|
|
30
|
+
if (
|
|
31
|
+
getCfg && putCfg &&
|
|
32
|
+
getCfg.clientKey && getCfg.serviceId &&
|
|
33
|
+
putCfg.clientKey && putCfg.serviceId
|
|
34
|
+
){
|
|
35
|
+
const user = dataLayer.getUser(store); // Get the user from the session;
|
|
36
|
+
|
|
37
|
+
// get the API endpoint URL, clientKey, serviceId from the environment variable (handle edge cases)
|
|
38
|
+
const getCfgUrl = getEnvVariable(getCfg?.url || "", false);
|
|
39
|
+
const getCfgClientKey = getEnvVariable(getCfg?.clientKey || "", false);
|
|
40
|
+
const getCfgServiceId = getEnvVariable(getCfg?.serviceId || "", false);
|
|
41
|
+
const getCfgDsfGtwApiKey = getEnvVariable(getCfg?.dsfgtwApiKey || "", "");
|
|
42
|
+
const getCfgParams = getCfg?.params || {};
|
|
43
|
+
const getCfgMethod = (getCfg?.method || "GET").toLowerCase();
|
|
44
|
+
const putCfgUrl = getEnvVariable(putCfg?.url || "", false);
|
|
45
|
+
const putCfgClientKey = getEnvVariable(putCfg?.clientKey || "", false);
|
|
46
|
+
const putCfgServiceId = getEnvVariable(putCfg?.serviceId || "", false);
|
|
47
|
+
const putCfgDsfGtwApiKey = getEnvVariable(putCfg?.dsfgtwApiKey || "", "");
|
|
48
|
+
const putCfgParams = putCfg?.params || {};
|
|
49
|
+
const putCfgMethod = (putCfg?.method || "PUT").toLowerCase();
|
|
50
|
+
|
|
51
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES",false) ; // Default to false if not set
|
|
52
|
+
|
|
53
|
+
// check necessary values exist
|
|
54
|
+
if (!getCfgUrl || !getCfgClientKey
|
|
55
|
+
|| !putCfgUrl || !putCfgClientKey ) {
|
|
56
|
+
|
|
57
|
+
return handleMiddlewareError(`π¨ Get submission environment variable missing:
|
|
58
|
+
getURl : ${getCfgUrl}, getClientKey : ${getCfgClientKey}
|
|
59
|
+
putURl : ${putCfgUrl}, putClientKey : ${putCfgClientKey}`
|
|
60
|
+
, 500, next)
|
|
61
|
+
}
|
|
62
|
+
// get response form GET submission API
|
|
63
|
+
const getResponse = await govcyApiRequest(
|
|
64
|
+
getCfgMethod,
|
|
65
|
+
getCfgUrl,
|
|
66
|
+
getCfgParams,
|
|
67
|
+
true, // use access token auth
|
|
68
|
+
user,
|
|
69
|
+
{
|
|
70
|
+
accept: "text/plain", // Set Accept header to text/plain
|
|
71
|
+
"client-key": getCfgClientKey, // Set the client key header
|
|
72
|
+
"service-id": getCfgServiceId, // Set the service ID header
|
|
73
|
+
...(getCfgDsfGtwApiKey !== '' && { "dsfgtw-api-key": getCfgDsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
|
|
74
|
+
},
|
|
75
|
+
3,
|
|
76
|
+
allowSelfSignedCerts,
|
|
77
|
+
[200, 404] // Allowed HTTP status codes
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// If not succeeded, handle error
|
|
81
|
+
if (!getResponse.Succeeded) {
|
|
82
|
+
logger.debug("govcyLoadSubmissionData returned succeeded false",getResponse)
|
|
83
|
+
return handleMiddlewareError(`π¨ govcyLoadSubmissionData returned succeeded false`, 500, next)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// check if getResponse.Data is defined
|
|
87
|
+
if (getResponse.Data) {
|
|
88
|
+
// Store the response in the request for later use
|
|
89
|
+
dataLayer.storeSiteLoadData(store, siteId, getResponse.Data);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const parsed = JSON.parse(getResponse.Data.submissionData
|
|
93
|
+
|| getResponse.Data.submission_data
|
|
94
|
+
|| "{}");
|
|
95
|
+
if (parsed && typeof parsed === "object") {
|
|
96
|
+
dataLayer.storeSiteInputData(store, siteId, parsed);
|
|
97
|
+
logger.debug(`πΎ Input data restored from saved submission for siteId: ${siteId}`);
|
|
98
|
+
}
|
|
99
|
+
} catch (err) {
|
|
100
|
+
logger.warn(`β οΈ Failed to parse saved submissionData for siteId: ${siteId}`, err);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// if not call the PUT submission API
|
|
104
|
+
} else {
|
|
105
|
+
const tempPutPayload = {
|
|
106
|
+
// submission_data: JSON.stringify({}),
|
|
107
|
+
submissionData: JSON.stringify({})
|
|
108
|
+
};
|
|
109
|
+
// If no data, call the PUT submission API to create it
|
|
110
|
+
const putResponse = await govcyApiRequest(
|
|
111
|
+
putCfgMethod,
|
|
112
|
+
putCfgUrl,
|
|
113
|
+
tempPutPayload,
|
|
114
|
+
true, // use access token auth
|
|
115
|
+
user,
|
|
116
|
+
{
|
|
117
|
+
accept: "text/plain", // Set Accept header to text/plain
|
|
118
|
+
"client-key": putCfgClientKey, // Set the client key header
|
|
119
|
+
"service-id": putCfgServiceId, // Set the service ID header
|
|
120
|
+
...(putCfgDsfGtwApiKey !== '' && { "dsfgtw-api-key": putCfgDsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
|
|
121
|
+
},
|
|
122
|
+
3,
|
|
123
|
+
allowSelfSignedCerts
|
|
124
|
+
);
|
|
125
|
+
// If not succeeded, handle error
|
|
126
|
+
if (!putResponse.Succeeded) {
|
|
127
|
+
logger.debug("govcyLoadSubmissionData returned succeeded false",putResponse)
|
|
128
|
+
return handleMiddlewareError(`π¨ govcyLoadSubmissionData returned succeeded false`, 500, next)
|
|
129
|
+
}
|
|
130
|
+
// Store the response in the request for later use
|
|
131
|
+
dataLayer.storeSiteLoadData(store, siteId, putResponse.Data);
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
next();
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logger.error("Error in govcyLoadSubmissionData middleware:", error.message);
|
|
140
|
+
return next(error); // Pass the error to the next middleware
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -66,6 +66,12 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
66
66
|
// Store in submissionData
|
|
67
67
|
submissionData[pageUrl].formData[elId] = value;
|
|
68
68
|
|
|
69
|
+
// handle fileInput
|
|
70
|
+
if (elType === "fileInput") {
|
|
71
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
72
|
+
submissionData[pageUrl].formData[elId + "Attachment"] = value;
|
|
73
|
+
delete submissionData[pageUrl].formData[elId];
|
|
74
|
+
}
|
|
69
75
|
|
|
70
76
|
// π If radios with conditionalElements, walk ALL options
|
|
71
77
|
if (elType === "radios" && Array.isArray(element.params?.items)) {
|
|
@@ -85,6 +91,12 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
85
91
|
|
|
86
92
|
// Store even if the field was not visible to user
|
|
87
93
|
submissionData[pageUrl].formData[condId] = condValue;
|
|
94
|
+
// handle fileInput
|
|
95
|
+
if (condType === "fileInput") {
|
|
96
|
+
// change the name of the key to include "Attachment" at the end but not have the original key
|
|
97
|
+
submissionData[pageUrl].formData[condId + "Attachment"] = condValue;
|
|
98
|
+
delete submissionData[pageUrl].formData[condId];
|
|
99
|
+
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
90
102
|
}
|
|
@@ -291,6 +303,10 @@ function getValue(formElement, pageUrl, req, siteId) {
|
|
|
291
303
|
let value = ""
|
|
292
304
|
if (formElement.element === "dateInput") {
|
|
293
305
|
value = getDateInputISO(pageUrl, formElement.params.name, req, siteId);
|
|
306
|
+
} else if (formElement.element === "fileInput") {
|
|
307
|
+
// unneeded handle of `Attachment` at the end
|
|
308
|
+
// value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name + "Attachment");
|
|
309
|
+
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
294
310
|
} else {
|
|
295
311
|
value = dataLayer.getFormDataValue(req.session, siteId, pageUrl, formElement.params.name);
|
|
296
312
|
}
|
|
@@ -340,6 +356,17 @@ function getValueLabel(formElement, value, pageUrl, req, siteId, service) {
|
|
|
340
356
|
return govcyResources.getSameMultilingualObject(service.site.languages, formattedDate);
|
|
341
357
|
}
|
|
342
358
|
|
|
359
|
+
// handle fileInput
|
|
360
|
+
if (formElement.element === "fileInput") {
|
|
361
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
362
|
+
if (value) {
|
|
363
|
+
return govcyResources.staticResources.text.fileUploaded;
|
|
364
|
+
} else {
|
|
365
|
+
return govcyResources.getSameMultilingualObject(service.site.languages, "");
|
|
366
|
+
// return govcyResources.staticResources.text.fileNotUploaded;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
343
370
|
// textInput, textArea, etc.
|
|
344
371
|
return govcyResources.getSameMultilingualObject(service.site.languages, value);
|
|
345
372
|
}
|
|
@@ -410,6 +437,33 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
|
|
|
410
437
|
};
|
|
411
438
|
}
|
|
412
439
|
|
|
440
|
+
/**
|
|
441
|
+
* Helper function to create a summary list item for file links.
|
|
442
|
+
* @param {object} key the key of multilingual object
|
|
443
|
+
* @param {string} value the value
|
|
444
|
+
* @param {string} siteId the site id
|
|
445
|
+
* @param {string} pageUrl the page url
|
|
446
|
+
* @param {string} elementName the element name
|
|
447
|
+
* @returns {object} the summary list item with file link
|
|
448
|
+
*/
|
|
449
|
+
function createSummaryListItemFileLink(key, value, siteId, pageUrl, elementName) {
|
|
450
|
+
return {
|
|
451
|
+
"key": key,
|
|
452
|
+
"value": [
|
|
453
|
+
{
|
|
454
|
+
"element": "htmlElement",
|
|
455
|
+
"params": {
|
|
456
|
+
"text": {
|
|
457
|
+
"en": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.en}<span class="govcy-visually-hidden"> ${key?.en || ""}</span></a>`,
|
|
458
|
+
"el": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.el}<span class="govcy-visually-hidden"> ${key?.el || ""}</span></a>`,
|
|
459
|
+
"tr": `<a href="/${siteId}/${pageUrl}/view-file/${elementName}" target="_blank">${govcyResources.staticResources.text.viewFile.tr}<span class="govcy-visually-hidden"> ${key?.tr || ""}</span></a>`
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
413
467
|
|
|
414
468
|
|
|
415
469
|
|
|
@@ -425,8 +479,14 @@ export function generateReviewSummary(submissionData, req, siteId, showChangeLin
|
|
|
425
479
|
for (const field of fields) {
|
|
426
480
|
const label = field.label;
|
|
427
481
|
const valueLabel = getSubmissionValueLabelString(field.valueLabel, req.globalLang);
|
|
428
|
-
//
|
|
429
|
-
|
|
482
|
+
// --- HACK --- to see if this is a file element
|
|
483
|
+
// check if field.value is an object with `sha256` and `fileId` properties
|
|
484
|
+
if (typeof field.value === "object" && field.value.hasOwnProperty("sha256") && field.value.hasOwnProperty("fileId") && showChangeLinks) {
|
|
485
|
+
summaryListInner.params.items.push(createSummaryListItemFileLink(label, valueLabel, siteId, pageUrl, field.name));
|
|
486
|
+
} else {
|
|
487
|
+
// add the field to the summary entry
|
|
488
|
+
summaryListInner.params.items.push(createSummaryListItem(label, valueLabel));
|
|
489
|
+
}
|
|
430
490
|
}
|
|
431
491
|
|
|
432
492
|
// Add inner summary list to the main summary list
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// utils/govcyTempSave.mjs
|
|
2
|
+
import { govcyApiRequest } from "./govcyApiRequest.mjs";
|
|
3
|
+
import { getEnvVariable, getEnvVariableBool } from "./govcyEnvVariables.mjs";
|
|
4
|
+
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
5
|
+
import { logger } from "./govcyLogger.mjs";
|
|
6
|
+
/**
|
|
7
|
+
* Temporary save of in-progress form data via configured API endpoints.
|
|
8
|
+
* @param {object} store The session store
|
|
9
|
+
* @param {object} service The service object
|
|
10
|
+
* @param {string} siteId The site id
|
|
11
|
+
*/
|
|
12
|
+
export async function tempSaveIfConfigured(store, service, siteId) {
|
|
13
|
+
// Check if temp save is configured for this service with a PUT endpoint
|
|
14
|
+
const putCfg = service?.site?.submissionPutAPIEndpoint;
|
|
15
|
+
if (!putCfg?.url || !putCfg?.clientKey || !putCfg?.serviceId) return;
|
|
16
|
+
|
|
17
|
+
//Get environment variables
|
|
18
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
19
|
+
const url = getEnvVariable(putCfg.url || "", false);
|
|
20
|
+
const clientKey = getEnvVariable(putCfg.clientKey || "", false);
|
|
21
|
+
const serviceId = getEnvVariable(putCfg.serviceId || "", false);
|
|
22
|
+
const dsfGtwKey = getEnvVariable(putCfg?.dsfgtwApiKey || "", "");
|
|
23
|
+
const method = (putCfg?.method || "PUT").toLowerCase();
|
|
24
|
+
const user = dataLayer.getUser(store);
|
|
25
|
+
|
|
26
|
+
// Prepare minimal temp payload (send whole inputData snapshot)
|
|
27
|
+
const inputData = dataLayer.getSiteInputData(store, siteId) || {};
|
|
28
|
+
const tempPayload = {
|
|
29
|
+
// mirror final submission format: send stringified JSON
|
|
30
|
+
// submission_data: JSON.stringify(inputData),
|
|
31
|
+
submissionData: JSON.stringify(inputData)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
if (!url || !clientKey) {
|
|
35
|
+
logger.error("π¨ Temp save API configuration is incomplete:", { url, clientKey })
|
|
36
|
+
return; // don't break UX
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
// Call the API to save temp data
|
|
41
|
+
const resp = await govcyApiRequest(
|
|
42
|
+
method,
|
|
43
|
+
url,
|
|
44
|
+
tempPayload,
|
|
45
|
+
true, // auth with user access token
|
|
46
|
+
user,
|
|
47
|
+
{
|
|
48
|
+
accept: "text/plain",
|
|
49
|
+
"client-key": clientKey,
|
|
50
|
+
"service-id": serviceId,
|
|
51
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey }),
|
|
52
|
+
"content-type": "application/json"
|
|
53
|
+
},
|
|
54
|
+
3,
|
|
55
|
+
allowSelfSignedCerts
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
if (!resp?.Succeeded) {
|
|
59
|
+
logger.warn("Temp save returned Succeeded=false", resp);
|
|
60
|
+
return; // donβt break UX
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
logger.info("β
Temp save successful for site:", siteId, "Response:", resp);
|
|
64
|
+
// Optional: reflect any server state locally (e.g., keep referenceValue in loadData)
|
|
65
|
+
if (resp?.Data) {
|
|
66
|
+
dataLayer.storeSiteLoadData(store, siteId,resp.Data );
|
|
67
|
+
}
|
|
68
|
+
} catch (e) {
|
|
69
|
+
logger.error("Temp save failed (non-blocking):", e?.message);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -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`
|