@gov-cy/govcy-express-services 1.0.0-alpha.1 → 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 +156 -11
- package/package.json +4 -2
- package/src/index.mjs +20 -1
- 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 +1 -1
- package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
- 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 +22 -0
- 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 +10 -3
- package/src/utils/govcySubmitData.mjs +62 -2
- package/src/utils/govcyTempSave.mjs +2 -1
- package/src/utils/govcyValidator.mjs +7 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The successResponse function creates a standardized success response object.
|
|
3
|
+
*
|
|
4
|
+
* @param {*} data - The data to be included in the response.
|
|
5
|
+
* @returns {Object} - The success response object.
|
|
6
|
+
*/
|
|
7
|
+
export function successResponse(data = null) {
|
|
8
|
+
return {
|
|
9
|
+
Succeeded: true,
|
|
10
|
+
ErrorCode: 0,
|
|
11
|
+
ErrorMessage: '',
|
|
12
|
+
Data: data
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The errorResponse function creates a standardized error response object.
|
|
18
|
+
*
|
|
19
|
+
* @param {int} code - The error code to be included in the response.
|
|
20
|
+
* @param {string} message - The error message to be included in the response.
|
|
21
|
+
* @param {Object} data - Additional data to be included in the response.
|
|
22
|
+
* @returns {Object} - The error response object.
|
|
23
|
+
*/
|
|
24
|
+
export function errorResponse(code = 1, message = 'Unknown error', data = null) {
|
|
25
|
+
return {
|
|
26
|
+
Succeeded: false,
|
|
27
|
+
ErrorCode: code,
|
|
28
|
+
ErrorMessage: message,
|
|
29
|
+
Data: data
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared constants for allowed form elements.
|
|
3
3
|
*/
|
|
4
|
-
export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios", "checkboxes", "datePicker", "dateInput"];
|
|
4
|
+
export const ALLOWED_FORM_ELEMENTS = ["textInput", "textArea", "select", "radios", "checkboxes", "datePicker", "dateInput","fileInput","fileView"];
|
|
5
|
+
export const ALLOWED_FILE_MIME_TYPES = ['application/pdf', 'image/jpeg', 'image/png'];
|
|
6
|
+
export const ALLOWED_FILE_EXTENSIONS = ['pdf', 'jpg', 'jpeg', 'png'];
|
|
7
|
+
export const ALLOWED_FILE_SIZE_MB = 5; // Maximum file size in MB
|
|
8
|
+
export const ALLOWED_MULTER_FILE_SIZE_MB = 10; // Maximum file size in MB
|
|
@@ -98,6 +98,14 @@ export function storePageData(store, siteId, pageUrl, formData) {
|
|
|
98
98
|
|
|
99
99
|
store.siteData[siteId].inputData[pageUrl]["formData"] = formData;
|
|
100
100
|
}
|
|
101
|
+
|
|
102
|
+
export function storePageDataElement(store, siteId, pageUrl, elementName, value) {
|
|
103
|
+
// Ensure session structure is initialized
|
|
104
|
+
initializeSiteData(store, siteId, pageUrl);
|
|
105
|
+
|
|
106
|
+
// Store the element value
|
|
107
|
+
store.siteData[siteId].inputData[pageUrl].formData[elementName] = value;
|
|
108
|
+
}
|
|
101
109
|
/**
|
|
102
110
|
* Stores the page's input data in the data layer
|
|
103
111
|
* *
|
|
@@ -328,6 +336,20 @@ export function getSiteLoadData(store, siteId) {
|
|
|
328
336
|
return null;
|
|
329
337
|
}
|
|
330
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Get the site's reference number from load data from the store
|
|
341
|
+
*
|
|
342
|
+
* @param {object} store The session store
|
|
343
|
+
* @param {string} siteId The site ID
|
|
344
|
+
* @returns {string|null} The reference number or null if not available
|
|
345
|
+
*/
|
|
346
|
+
export function getSiteLoadDataReferenceNumber(store, siteId) {
|
|
347
|
+
const ref = store?.siteData?.[siteId]?.loadData?.referenceValue;
|
|
348
|
+
|
|
349
|
+
return typeof ref === 'string' && ref.trim() !== '' ? ref : null;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
|
|
331
353
|
/**
|
|
332
354
|
* Get the site's input data from the store
|
|
333
355
|
*
|
|
@@ -124,7 +124,7 @@ export function evaluateExpressionWithFlattening(expression, object, prefix = ''
|
|
|
124
124
|
* @returns {{ result: true } | { result: false, redirect: string }}
|
|
125
125
|
* An object indicating whether to render the page or redirect
|
|
126
126
|
*/
|
|
127
|
-
export function evaluatePageConditions(page, store, siteKey, req) {
|
|
127
|
+
export function evaluatePageConditions(page, store, siteKey, req = {}) {
|
|
128
128
|
// Get conditions array from nested page structure
|
|
129
129
|
const conditions = page?.pageData?.conditions;
|
|
130
130
|
|
|
@@ -5,16 +5,28 @@
|
|
|
5
5
|
* and show error summary when there are validation errors.
|
|
6
6
|
*/
|
|
7
7
|
import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
|
|
8
|
+
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
9
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
8
10
|
|
|
9
11
|
|
|
10
12
|
/**
|
|
11
13
|
* Helper function to populate form data with session data
|
|
12
14
|
* @param {Array} formElements The form elements
|
|
13
15
|
* @param {*} theData The data either from session or request
|
|
16
|
+
* @param {Object} validationErrors The validation errors
|
|
17
|
+
* @param {Object} store The session store
|
|
18
|
+
* @param {string} siteId The site ID
|
|
19
|
+
* @param {string} pageUrl The page URL
|
|
20
|
+
* @param {string} lang The language
|
|
21
|
+
* @param {Object} fileInputElements The file input elements
|
|
22
|
+
* @param {string} routeParam The route parameter
|
|
14
23
|
*/
|
|
15
|
-
export function populateFormData(formElements, theData, validationErrors) {
|
|
24
|
+
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el", fileInputElements = null, routeParam = "") {
|
|
16
25
|
const inputElements = ALLOWED_FORM_ELEMENTS;
|
|
17
|
-
|
|
26
|
+
const isRootCall = !fileInputElements;
|
|
27
|
+
if (isRootCall) {
|
|
28
|
+
fileInputElements = {};
|
|
29
|
+
}
|
|
18
30
|
// Recursively populate form data with session data
|
|
19
31
|
formElements.forEach(element => {
|
|
20
32
|
if (inputElements.includes(element.element)) {
|
|
@@ -53,6 +65,27 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
53
65
|
// Invalid format (not matching D/M/YYYY or DD/MM/YYYY)
|
|
54
66
|
element.params.value = "";
|
|
55
67
|
}
|
|
68
|
+
} else if (element.element === "fileInput") {
|
|
69
|
+
// For fileInput, we change the element.element to "fileView" and set the
|
|
70
|
+
// fileId and sha256 from the session store
|
|
71
|
+
// unneeded handle of `Attachment` at the end
|
|
72
|
+
// const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
|
|
73
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName);
|
|
74
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
75
|
+
if (fileData) {
|
|
76
|
+
element.element = "fileView";
|
|
77
|
+
element.params.fileId = fileData.fileId;
|
|
78
|
+
element.params.sha256 = fileData.sha256;
|
|
79
|
+
element.params.visuallyHiddenText = element.params.label;
|
|
80
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
81
|
+
element.params.viewHref = `/${siteId}/${pageUrl}/view-file/${fieldName}`;
|
|
82
|
+
element.params.viewTarget = "_blank";
|
|
83
|
+
element.params.deleteHref = `/${siteId}/${pageUrl}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`;
|
|
84
|
+
} else {
|
|
85
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
86
|
+
element.params.value = "";
|
|
87
|
+
}
|
|
88
|
+
fileInputElements[fieldName] = element;
|
|
56
89
|
// Handle all other input elements (textInput, checkboxes, radios, etc.)
|
|
57
90
|
} else {
|
|
58
91
|
element.params.value = theData[fieldName] || "";
|
|
@@ -78,7 +111,7 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
78
111
|
if (element.element === "radios" && element.params.items) {
|
|
79
112
|
element.params.items.forEach(item => {
|
|
80
113
|
if (item.conditionalElements) {
|
|
81
|
-
populateFormData(item.conditionalElements, theData, validationErrors);
|
|
114
|
+
populateFormData(item.conditionalElements, theData, validationErrors,store, siteId , pageUrl, lang, fileInputElements, routeParam);
|
|
82
115
|
|
|
83
116
|
// Check if any conditional element has an error and add to the parent "conditionalHasErrors": true
|
|
84
117
|
if (item.conditionalElements.some(condEl => condEl.params?.error)) {
|
|
@@ -88,6 +121,29 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
88
121
|
});
|
|
89
122
|
}
|
|
90
123
|
});
|
|
124
|
+
// add file input elements's definition in js object
|
|
125
|
+
if (isRootCall && Object.keys(fileInputElements).length > 0) {
|
|
126
|
+
const scriptTag = `
|
|
127
|
+
<script type="text/javascript">
|
|
128
|
+
window._govcyFileInputs = ${JSON.stringify(fileInputElements)};
|
|
129
|
+
window._govcySiteId = "${siteId}";
|
|
130
|
+
window._govcyPageUrl = "${pageUrl}";
|
|
131
|
+
window._govcyLang = "${lang}";
|
|
132
|
+
</script>
|
|
133
|
+
<div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="assertive"></div>
|
|
134
|
+
<div id="_govcy-upload-error" class="govcy-visually-hidden" role="alert" aria-live="assertive"></div>
|
|
135
|
+
`.trim();
|
|
136
|
+
formElements.push({
|
|
137
|
+
element: 'htmlElement',
|
|
138
|
+
params: {
|
|
139
|
+
text: {
|
|
140
|
+
en: scriptTag,
|
|
141
|
+
el: scriptTag,
|
|
142
|
+
tr: scriptTag
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
91
147
|
}
|
|
92
148
|
|
|
93
149
|
|
|
@@ -96,9 +152,12 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
96
152
|
*
|
|
97
153
|
* @param {Array} elements - The form elements (including conditional ones).
|
|
98
154
|
* @param {Object} formData - The submitted form data.
|
|
155
|
+
* @param {Object} store - The session store .
|
|
156
|
+
* @param {string} siteId - The site ID .
|
|
157
|
+
* @param {string} pageUrl - The page URL .
|
|
99
158
|
* @returns {Object} filteredData - The filtered form data.
|
|
100
159
|
*/
|
|
101
|
-
export function getFormData(elements, formData) {
|
|
160
|
+
export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "") {
|
|
102
161
|
const filteredData = {};
|
|
103
162
|
elements.forEach(element => {
|
|
104
163
|
const { name } = element.params || {};
|
|
@@ -115,7 +174,7 @@ export function getFormData(elements, formData) {
|
|
|
115
174
|
if (item.conditionalElements) {
|
|
116
175
|
Object.assign(
|
|
117
176
|
filteredData,
|
|
118
|
-
getFormData(item.conditionalElements, formData)
|
|
177
|
+
getFormData(item.conditionalElements, formData, store, siteId, pageUrl)
|
|
119
178
|
);
|
|
120
179
|
}
|
|
121
180
|
});
|
|
@@ -130,6 +189,23 @@ export function getFormData(elements, formData) {
|
|
|
130
189
|
filteredData[`${name}_day`] = day !== undefined && day !== null ? day : "";
|
|
131
190
|
filteredData[`${name}_month`] = month !== undefined && month !== null ? month : "";
|
|
132
191
|
filteredData[`${name}_year`] = year !== undefined && year !== null ? year : "";
|
|
192
|
+
// handle fileInput
|
|
193
|
+
} else if (element.element === "fileInput") {
|
|
194
|
+
// fileInput elements are already stored in the store when it was uploaded
|
|
195
|
+
// so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
|
|
196
|
+
// unneeded handle of `Attachment` at the end
|
|
197
|
+
// const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
|
|
198
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name);
|
|
199
|
+
if (fileData) {
|
|
200
|
+
// unneeded handle of `Attachment` at the end
|
|
201
|
+
// filteredData[name + "Attachment"] = fileData;
|
|
202
|
+
filteredData[name] = fileData;
|
|
203
|
+
} else {
|
|
204
|
+
//TODO: Ask Andreas how to handle empty file inputs
|
|
205
|
+
// unneeded handle of `Attachment` at the end
|
|
206
|
+
// filteredData[name + "Attachment"] = ""; // or handle as needed
|
|
207
|
+
filteredData[name] = ""; // or handle as needed
|
|
208
|
+
}
|
|
133
209
|
// Handle other elements (e.g., textInput, textArea, datePicker)
|
|
134
210
|
} else {
|
|
135
211
|
filteredData[name] = formData[name] !== undefined && formData[name] !== null ? formData[name] : "";
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import FormData from 'form-data';
|
|
2
|
+
import { getPageConfigData } from "./govcyLoadConfigData.mjs";
|
|
3
|
+
import { evaluatePageConditions } from "./govcyExpressions.mjs";
|
|
4
|
+
import { getEnvVariable, getEnvVariableBool } from "./govcyEnvVariables.mjs";
|
|
5
|
+
import { ALLOWED_FILE_MIME_TYPES, ALLOWED_FILE_SIZE_MB } from "./govcyConstants.mjs";
|
|
6
|
+
import { govcyApiRequest } from "./govcyApiRequest.mjs";
|
|
7
|
+
import * as dataLayer from "./govcyDataLayer.mjs";
|
|
8
|
+
import { logger } from './govcyLogger.mjs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handles the logic for uploading a file to the configured Upload API.
|
|
12
|
+
* Does not send a response — just returns a standard object to be handled by middleware.
|
|
13
|
+
*
|
|
14
|
+
* @param {object} opts - Input parameters
|
|
15
|
+
* @param {object} opts.service - The service config object
|
|
16
|
+
* @param {object} opts.store - Session store (req.session)
|
|
17
|
+
* @param {string} opts.siteId - Site ID
|
|
18
|
+
* @param {string} opts.pageUrl - Page URL
|
|
19
|
+
* @param {string} opts.elementName - Name of file input
|
|
20
|
+
* @param {object} opts.file - File object from multer (req.file)
|
|
21
|
+
* @returns {Promise<{ status: number, data?: object, errorMessage?: string }>}
|
|
22
|
+
*/
|
|
23
|
+
export async function handleFileUpload({ service, store, siteId, pageUrl, elementName, file }) {
|
|
24
|
+
try {
|
|
25
|
+
// Validate essentials
|
|
26
|
+
// Early exit if key things are missing
|
|
27
|
+
if (!file || !elementName) {
|
|
28
|
+
return {
|
|
29
|
+
status: 400,
|
|
30
|
+
dataStatus: 400,
|
|
31
|
+
errorMessage: 'Missing file or element name'
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get the upload configuration
|
|
36
|
+
const uploadCfg = service?.site?.fileUploadAPIEndpoint;
|
|
37
|
+
// Check if upload configuration is available
|
|
38
|
+
if (!uploadCfg?.url || !uploadCfg?.clientKey || !uploadCfg?.serviceId) {
|
|
39
|
+
return {
|
|
40
|
+
status: 400,
|
|
41
|
+
dataStatus: 401,
|
|
42
|
+
errorMessage: 'Missing upload configuration'
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Environment vars
|
|
47
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
48
|
+
let url = getEnvVariable(uploadCfg.url || "", false);
|
|
49
|
+
const clientKey = getEnvVariable(uploadCfg.clientKey || "", false);
|
|
50
|
+
const serviceId = getEnvVariable(uploadCfg.serviceId || "", false);
|
|
51
|
+
const dsfGtwKey = getEnvVariable(uploadCfg?.dsfgtwApiKey || "", "");
|
|
52
|
+
const method = (uploadCfg?.method || "PUT").toLowerCase();
|
|
53
|
+
|
|
54
|
+
// Check if the upload API is configured correctly
|
|
55
|
+
if (!url || !clientKey) {
|
|
56
|
+
return {
|
|
57
|
+
status: 400,
|
|
58
|
+
dataStatus: 402,
|
|
59
|
+
errorMessage: 'Missing environment variables for upload'
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Construct the URL with tag being the elementName
|
|
64
|
+
const tag = encodeURIComponent(elementName.trim());
|
|
65
|
+
url += `/${tag}`;
|
|
66
|
+
|
|
67
|
+
// Get the page configuration using utility (safely extracts the correct page)
|
|
68
|
+
const page = getPageConfigData(service, pageUrl);
|
|
69
|
+
// Check if the page template is valid
|
|
70
|
+
if (!page?.pageTemplate) {
|
|
71
|
+
return {
|
|
72
|
+
status: 400,
|
|
73
|
+
dataStatus: 403,
|
|
74
|
+
errorMessage: 'Invalid page configuration'
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ----- Conditional logic comes here
|
|
79
|
+
// Respect conditional logic: If the page is skipped due to conditions, abort
|
|
80
|
+
const conditionResult = evaluatePageConditions(page, store, siteId);
|
|
81
|
+
if (conditionResult.result === false) {
|
|
82
|
+
return {
|
|
83
|
+
status: 403,
|
|
84
|
+
dataStatus: 404,
|
|
85
|
+
errorMessage: 'This page is skipped by conditional logic'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// deep copy the page template to avoid modifying the original
|
|
90
|
+
const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
|
|
91
|
+
// Validate the field: Only allow upload if the page contains a fileInput with the given name
|
|
92
|
+
const isAllowed = pageContainsFileInput(pageTemplateCopy, elementName);
|
|
93
|
+
if (!isAllowed) {
|
|
94
|
+
return {
|
|
95
|
+
status: 403,
|
|
96
|
+
dataStatus: 405,
|
|
97
|
+
errorMessage: `File input [${elementName}] not allowed on this page`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Empty file check
|
|
102
|
+
if (file.size === 0) {
|
|
103
|
+
return {
|
|
104
|
+
status: 400,
|
|
105
|
+
dataStatus: 406,
|
|
106
|
+
errorMessage: 'Uploaded file is empty'
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// file type checks
|
|
111
|
+
// 1. Check declared mimetype
|
|
112
|
+
if (!ALLOWED_FILE_MIME_TYPES.includes(file.mimetype)) {
|
|
113
|
+
return {
|
|
114
|
+
status: 400,
|
|
115
|
+
dataStatus: 407,
|
|
116
|
+
errorMessage: 'Invalid file type (MIME not allowed)'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Check actual file content (magic bytes) matches claimed MIME type
|
|
121
|
+
if (!isMagicByteValid(file.buffer, file.mimetype)) {
|
|
122
|
+
return {
|
|
123
|
+
status: 400,
|
|
124
|
+
dataStatus: 408,
|
|
125
|
+
errorMessage: 'Invalid file type (magic byte mismatch)'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// File size check
|
|
130
|
+
if (file.size > ALLOWED_FILE_SIZE_MB * 1024 * 1024) {
|
|
131
|
+
return {
|
|
132
|
+
status: 400,
|
|
133
|
+
dataStatus: 409,
|
|
134
|
+
errorMessage: 'File exceeds allowed size'
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Prepare FormData
|
|
139
|
+
const form = new FormData();
|
|
140
|
+
form.append('file', file.buffer, {
|
|
141
|
+
filename: file.originalname,
|
|
142
|
+
contentType: file.mimetype,
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
logger.debug("Prepared FormData with file:", {
|
|
146
|
+
filename: file.originalname,
|
|
147
|
+
mimetype: file.mimetype,
|
|
148
|
+
size: file.size
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Get the user
|
|
152
|
+
const user = dataLayer.getUser(store);
|
|
153
|
+
// Perform the upload request
|
|
154
|
+
const response = await govcyApiRequest(
|
|
155
|
+
method,
|
|
156
|
+
url,
|
|
157
|
+
form,
|
|
158
|
+
true,
|
|
159
|
+
user,
|
|
160
|
+
{
|
|
161
|
+
accept: "text/plain",
|
|
162
|
+
"client-key": clientKey,
|
|
163
|
+
"service-id": serviceId,
|
|
164
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
|
|
165
|
+
},
|
|
166
|
+
3,
|
|
167
|
+
allowSelfSignedCerts
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// If not succeeded, handle error
|
|
171
|
+
if (!response?.Succeeded) {
|
|
172
|
+
return {
|
|
173
|
+
status: 500,
|
|
174
|
+
dataStatus: 410,
|
|
175
|
+
errorMessage: `${response?.ErrorCode} - ${response?.ErrorMessage} - fileUploadAPIEndpoint returned succeeded false`
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Check if the response contains the expected data
|
|
180
|
+
if (!response?.Data?.fileId || !response?.Data?.sha256) {
|
|
181
|
+
return {
|
|
182
|
+
status: 500,
|
|
183
|
+
dataStatus: 411,
|
|
184
|
+
errorMessage: 'Missing fileId or sha256 in response'
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ✅ Success
|
|
189
|
+
// Store the file metadata in the session store
|
|
190
|
+
// unneeded handle of `Attachment` at the end
|
|
191
|
+
// dataLayer.storePageDataElement(store, siteId, pageUrl, elementName+"Attachment", {
|
|
192
|
+
dataLayer.storePageDataElement(store, siteId, pageUrl, elementName, {
|
|
193
|
+
sha256: response.Data.sha256,
|
|
194
|
+
fileId: response.Data.fileId,
|
|
195
|
+
});
|
|
196
|
+
logger.debug("File upload successful", response.Data);
|
|
197
|
+
logger.info(`File uploaded successfully for element ${elementName} on page ${pageUrl} for site ${siteId}`);
|
|
198
|
+
return {
|
|
199
|
+
status: 200,
|
|
200
|
+
data: {
|
|
201
|
+
sha256: response.Data.sha256,
|
|
202
|
+
filename: response.Data.fileName || '',
|
|
203
|
+
fileId: response.Data.fileId,
|
|
204
|
+
mimeType: response.Data.contentType || '',
|
|
205
|
+
fileSize: response.Data?.fileSize || ''
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
} catch (err) {
|
|
210
|
+
return {
|
|
211
|
+
status: 500,
|
|
212
|
+
dataStatus: 500,
|
|
213
|
+
errorMessage: 'Upload failed' + (err.message ? `: ${err.message}` : ''),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
//--------------------------------------------------------------------------
|
|
219
|
+
// Helper Functions
|
|
220
|
+
/**
|
|
221
|
+
* Recursively checks whether any element (or its children) is a fileInput
|
|
222
|
+
* with the matching elementName.
|
|
223
|
+
*
|
|
224
|
+
* Supports:
|
|
225
|
+
* - Top-level fileInput
|
|
226
|
+
* - Nested `params.elements` (used in groups, conditionals, etc.)
|
|
227
|
+
* - Conditional radios/checkboxes with `items[].conditionalElements`
|
|
228
|
+
*
|
|
229
|
+
* @param {Array} elements - The array of elements to search
|
|
230
|
+
* @param {string} targetName - The name of the file input to check
|
|
231
|
+
* @returns {boolean} True if a matching fileInput is found, false otherwise
|
|
232
|
+
*/
|
|
233
|
+
function containsFileInput(elements = [], targetName) {
|
|
234
|
+
for (const el of elements) {
|
|
235
|
+
// ✅ Direct file input match
|
|
236
|
+
if (el.element === 'fileInput' && el.params?.name === targetName) {
|
|
237
|
+
return el;
|
|
238
|
+
}
|
|
239
|
+
// 🔁 Recurse into nested elements (e.g. groups, conditionals)
|
|
240
|
+
if (Array.isArray(el?.params?.elements)) {
|
|
241
|
+
const nestedMatch = containsFileInput(el.params.elements, targetName);
|
|
242
|
+
if (nestedMatch) return nestedMatch; // ← propagate the found element
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// 🎯 Special case: conditional radios/checkboxes
|
|
246
|
+
if (
|
|
247
|
+
(el.element === 'radios' || el.element === 'checkboxes') &&
|
|
248
|
+
Array.isArray(el?.params?.items)
|
|
249
|
+
) {
|
|
250
|
+
for (const item of el.params.items) {
|
|
251
|
+
if (Array.isArray(item?.conditionalElements)) {
|
|
252
|
+
const match = containsFileInput(item.conditionalElements, targetName);
|
|
253
|
+
if (match) return match; // ← propagate the found element
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Checks whether the specified page contains a valid fileInput for this element ID
|
|
263
|
+
* under any <form> element in its sections
|
|
264
|
+
*
|
|
265
|
+
* @param {object} pageTemplate The page template object
|
|
266
|
+
* @param {string} elementName The name of the element to check
|
|
267
|
+
* @return {boolean} True if a fileInput exists, false otherwise
|
|
268
|
+
*/
|
|
269
|
+
export function pageContainsFileInput(pageTemplate, elementName) {
|
|
270
|
+
const sections = pageTemplate?.sections || [];
|
|
271
|
+
|
|
272
|
+
for (const section of sections) {
|
|
273
|
+
for (const el of section?.elements || []) {
|
|
274
|
+
if (el.element === 'form') {
|
|
275
|
+
const match = containsFileInput(el.params?.elements, elementName);
|
|
276
|
+
if (match) {
|
|
277
|
+
return match; // ← return the actual element
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return null; // no match found
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Validates magic bytes against expected mimetype
|
|
289
|
+
* @param {Buffer} buffer
|
|
290
|
+
* @param {string} mimetype
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
export function isMagicByteValid(buffer, mimetype) {
|
|
294
|
+
const signatures = {
|
|
295
|
+
'application/pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
|
|
296
|
+
'image/png': [0x89, 0x50, 0x4E, 0x47], // PNG
|
|
297
|
+
'image/jpeg': [0xFF, 0xD8, 0xFF], // JPG/JPEG
|
|
298
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [0x50, 0x4B, 0x03, 0x04], // DOCX
|
|
299
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [0x50, 0x4B, 0x03, 0x04], // XLSX
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
const expected = signatures[mimetype];
|
|
303
|
+
if (!expected) return false; // unknown type
|
|
304
|
+
|
|
305
|
+
const actual = Array.from(buffer.slice(0, expected.length));
|
|
306
|
+
return expected.every((byte, i) => actual[i] === byte);
|
|
307
|
+
}
|
|
@@ -73,7 +73,8 @@ export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next)
|
|
|
73
73
|
...(getCfgDsfGtwApiKey !== '' && { "dsfgtw-api-key": getCfgDsfGtwApiKey }) // Use the DSF API GTW secret from environment variables
|
|
74
74
|
},
|
|
75
75
|
3,
|
|
76
|
-
allowSelfSignedCerts
|
|
76
|
+
allowSelfSignedCerts,
|
|
77
|
+
[200, 404] // Allowed HTTP status codes
|
|
77
78
|
);
|
|
78
79
|
|
|
79
80
|
// If not succeeded, handle error
|
|
@@ -88,7 +89,9 @@ export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next)
|
|
|
88
89
|
dataLayer.storeSiteLoadData(store, siteId, getResponse.Data);
|
|
89
90
|
|
|
90
91
|
try {
|
|
91
|
-
const parsed = JSON.parse(getResponse.Data.submissionData
|
|
92
|
+
const parsed = JSON.parse(getResponse.Data.submissionData
|
|
93
|
+
|| getResponse.Data.submission_data
|
|
94
|
+
|| "{}");
|
|
92
95
|
if (parsed && typeof parsed === "object") {
|
|
93
96
|
dataLayer.storeSiteInputData(store, siteId, parsed);
|
|
94
97
|
logger.debug(`💾 Input data restored from saved submission for siteId: ${siteId}`);
|
|
@@ -99,11 +102,15 @@ export async function govcyLoadSubmissionDataAPIs(store, service, siteId, next)
|
|
|
99
102
|
|
|
100
103
|
// if not call the PUT submission API
|
|
101
104
|
} else {
|
|
105
|
+
const tempPutPayload = {
|
|
106
|
+
// submission_data: JSON.stringify({}),
|
|
107
|
+
submissionData: JSON.stringify({})
|
|
108
|
+
};
|
|
102
109
|
// If no data, call the PUT submission API to create it
|
|
103
110
|
const putResponse = await govcyApiRequest(
|
|
104
111
|
putCfgMethod,
|
|
105
112
|
putCfgUrl,
|
|
106
|
-
|
|
113
|
+
tempPutPayload,
|
|
107
114
|
true, // use access token auth
|
|
108
115
|
user,
|
|
109
116
|
{
|
|
@@ -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
|