@gov-cy/govcy-express-services 1.0.0-alpha.5 → 1.0.0-alpha.7
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/package.json +4 -2
- package/src/index.mjs +9 -1
- package/src/middleware/cyLoginAuth.mjs +8 -0
- package/src/middleware/govcyCsrf.mjs +15 -1
- package/src/middleware/govcyFormsPostHandler.mjs +1 -1
- package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
- package/src/middleware/govcyPageHandler.mjs +4 -1
- package/src/middleware/govcyUpload.mjs +36 -0
- package/src/public/js/govcyFiles.js +152 -0
- package/src/resources/govcyResources.mjs +24 -3
- package/src/utils/govcyApiDetection.mjs +17 -0
- package/src/utils/govcyApiRequest.mjs +19 -2
- 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 +64 -3
- package/src/utils/govcyHandleFiles.mjs +282 -0
- package/src/utils/govcyValidator.mjs +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gov-cy/govcy-express-services",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.7",
|
|
4
4
|
"description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
|
|
5
5
|
"author": "DMRID - DSF Team",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,12 +48,14 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@gov-cy/dsf-email-templates": "^2.1.0",
|
|
51
|
-
"@gov-cy/govcy-frontend-renderer": "^1.
|
|
51
|
+
"@gov-cy/govcy-frontend-renderer": "^1.20.0",
|
|
52
52
|
"axios": "^1.9.0",
|
|
53
53
|
"cookie-parser": "^1.4.7",
|
|
54
54
|
"dotenv": "^16.3.1",
|
|
55
55
|
"express": "^4.18.2",
|
|
56
56
|
"express-session": "^1.17.3",
|
|
57
|
+
"form-data": "^4.0.4",
|
|
58
|
+
"multer": "^2.0.2",
|
|
57
59
|
"openid-client": "^6.3.4",
|
|
58
60
|
"puppeteer": "^24.6.0"
|
|
59
61
|
},
|
package/src/index.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { govcyManifestHandler } from './middleware/govcyManifestHandler.mjs';
|
|
|
24
24
|
import { govcyRoutePageHandler } from './middleware/govcyRoutePageHandler.mjs';
|
|
25
25
|
import { govcyServiceEligibilityHandler } from './middleware/govcyServiceEligibilityHandler.mjs';
|
|
26
26
|
import { govcyLoadSubmissionData } from './middleware/govcyLoadSubmissionData.mjs';
|
|
27
|
+
import { govcyUploadMiddleware } from './middleware/govcyUpload.mjs';
|
|
27
28
|
import { isProdOrStaging , getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
|
|
28
29
|
import { logger } from "./utils/govcyLogger.mjs";
|
|
29
30
|
|
|
@@ -128,6 +129,14 @@ export default function initializeGovCyExpressService(){
|
|
|
128
129
|
|
|
129
130
|
// 📝 -- ROUTE: Serve manifest.json dynamically for each site
|
|
130
131
|
app.get('/:siteId/manifest.json', serviceConfigDataMiddleware, govcyManifestHandler());
|
|
132
|
+
|
|
133
|
+
// 🗃️ -- ROUTE: Handle POST requests for file uploads for a page.
|
|
134
|
+
app.post('/apis/:siteId/:pageUrl/upload',
|
|
135
|
+
serviceConfigDataMiddleware,
|
|
136
|
+
requireAuth, // UNCOMMENT
|
|
137
|
+
naturalPersonPolicy, // UNCOMMENT
|
|
138
|
+
govcyServiceEligibilityHandler(true), // UNCOMMENT
|
|
139
|
+
govcyUploadMiddleware);
|
|
131
140
|
|
|
132
141
|
// 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
|
|
133
142
|
app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyLoadSubmissionData(),govcyPageHandler(), renderGovcyPage());
|
|
@@ -150,7 +159,6 @@ export default function initializeGovCyExpressService(){
|
|
|
150
159
|
// 👀📥 -- ROUTE: Handle POST requests (Form Submissions) based on siteId and pageUrl, using govcyFormsPostHandler middleware
|
|
151
160
|
app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
|
|
152
161
|
|
|
153
|
-
|
|
154
162
|
// post for /:siteId/review
|
|
155
163
|
|
|
156
164
|
// 🔹 Catch 404 errors (must be after all routes)
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
import { getLoginUrl, handleCallback, getLogoutUrl } from '../auth/cyLoginAuth.mjs';
|
|
8
8
|
import { logger } from "../utils/govcyLogger.mjs";
|
|
9
9
|
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
10
|
+
import { errorResponse } from "../utils/govcyApiResponse.mjs";
|
|
11
|
+
import { isApiRequest } from '../utils/govcyApiDetection.mjs';
|
|
10
12
|
|
|
11
13
|
/**
|
|
12
14
|
* Middleware to check if the user is authenticated. If not, redirect to the login page.
|
|
@@ -17,6 +19,12 @@ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
|
17
19
|
*/
|
|
18
20
|
export function requireAuth(req, res, next) {
|
|
19
21
|
if (!req.session.user) {
|
|
22
|
+
if (isApiRequest(req)) {
|
|
23
|
+
const err = new Error("Unauthorized: user not authenticated");
|
|
24
|
+
err.status = 401;
|
|
25
|
+
return next(err);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
// Store the original URL before redirecting to login
|
|
21
29
|
req.session.redirectAfterLogin = req.originalUrl;
|
|
22
30
|
return res.redirect('/login');
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
3
|
+
import { errorResponse } from '../utils/govcyApiResponse.mjs';
|
|
4
|
+
import { isApiRequest } from '../utils/govcyApiDetection.mjs';
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Middleware to handle CSRF token generation and validation.
|
|
5
8
|
*
|
|
@@ -14,7 +17,18 @@ export function govcyCsrfMiddleware(req, res, next) {
|
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
req.csrfToken = () => req.session.csrfToken;
|
|
17
|
-
|
|
20
|
+
|
|
21
|
+
if (
|
|
22
|
+
req.method === 'POST' &&
|
|
23
|
+
req.headers['content-type']?.includes('multipart/form-data') &&
|
|
24
|
+
isApiRequest(req)) {
|
|
25
|
+
const tokenFromHeader = req.get('X-CSRF-Token');
|
|
26
|
+
// UNCOMMENT
|
|
27
|
+
if (!tokenFromHeader || tokenFromHeader !== req.session.csrfToken) {
|
|
28
|
+
return res.status(400).json(errorResponse(403, 'Invalid CSRF token'));
|
|
29
|
+
}
|
|
30
|
+
return next();
|
|
31
|
+
}
|
|
18
32
|
// Check token on POST requests
|
|
19
33
|
if (req.method === 'POST') {
|
|
20
34
|
const tokenFromBody = req.body._csrf;
|
|
@@ -43,7 +43,7 @@ export function govcyFormsPostHandler() {
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
// const formData = req.body; // Submitted data
|
|
46
|
-
const formData = getFormData(formElement.params.elements, req.body); // Submitted data
|
|
46
|
+
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data
|
|
47
47
|
|
|
48
48
|
// ☑️ Start validation from top-level form elements
|
|
49
49
|
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
@@ -3,6 +3,8 @@ import * as govcyResources from "../resources/govcyResources.mjs";
|
|
|
3
3
|
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
4
4
|
import { logger } from "../utils/govcyLogger.mjs";
|
|
5
5
|
import { whatsIsMyEnvironment } from '../utils/govcyEnvVariables.mjs';
|
|
6
|
+
import { errorResponse } from '../utils/govcyApiResponse.mjs';
|
|
7
|
+
import { isApiRequest } from '../utils/govcyApiDetection.mjs';
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Middleware function to handle HTTP errors and render appropriate error pages.
|
|
@@ -49,9 +51,8 @@ export function govcyHttpErrorHandler(err, req, res, next) {
|
|
|
49
51
|
|
|
50
52
|
res.status(statusCode);
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
return res.json({ error: message });
|
|
54
|
+
if (isApiRequest(req)) {
|
|
55
|
+
return res.status(statusCode).json(errorResponse(statusCode, message));
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
// Render an error page for non-JSON requests
|
|
@@ -53,6 +53,9 @@ export function govcyPageHandler() {
|
|
|
53
53
|
element.params.method = "POST";
|
|
54
54
|
// ➕ Add CSRF token
|
|
55
55
|
element.params.elements.push(govcyResources.csrfTokenInput(req.csrfToken()));
|
|
56
|
+
// // ➕ Add siteId and pageUrl to form data
|
|
57
|
+
// element.params.elements.push(govcyResources.siteAndPageInput(siteId, pageUrl, req.globalLang));
|
|
58
|
+
// ➕ Add govcyFormsJs script to the form
|
|
56
59
|
element.params.elements.push(govcyResources.staticResources.elements["govcyFormsJs"]);
|
|
57
60
|
|
|
58
61
|
// 🔍 Find the first button with `prototypeNavigate`
|
|
@@ -88,7 +91,7 @@ export function govcyPageHandler() {
|
|
|
88
91
|
}
|
|
89
92
|
//--------- End of Handle Validation Errors ---------
|
|
90
93
|
|
|
91
|
-
populateFormData(element.params.elements, theData,validationErrors);
|
|
94
|
+
populateFormData(element.params.elements, theData,validationErrors, req.session, siteId, pageUrl, req.globalLang);
|
|
92
95
|
// if there are validation errors, add an error summary
|
|
93
96
|
if (validationErrors?.errorSummary?.length > 0) {
|
|
94
97
|
element.params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import multer from 'multer';
|
|
2
|
+
import { logger } from '../utils/govcyLogger.mjs';
|
|
3
|
+
import { successResponse, errorResponse } from '../utils/govcyApiResponse.mjs';
|
|
4
|
+
import { ALLOWED_MULTER_FILE_SIZE_MB } from "../utils/govcyConstants.mjs";
|
|
5
|
+
import { handleFileUpload } from "../utils/govcyHandleFiles.mjs";
|
|
6
|
+
|
|
7
|
+
// Configure multer to store the file in memory (not disk) and limit the size to 10MB
|
|
8
|
+
const upload = multer({
|
|
9
|
+
storage: multer.memoryStorage(),
|
|
10
|
+
limits: { fileSize: ALLOWED_MULTER_FILE_SIZE_MB * 1024 * 1024 } // 10MB
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
export const govcyUploadMiddleware = [
|
|
17
|
+
upload.single('file'), // multer parses the uploaded file and stores it in req.file
|
|
18
|
+
|
|
19
|
+
async function govcyUploadHandler(req, res) {
|
|
20
|
+
const result = await handleFileUpload({
|
|
21
|
+
service: req.serviceData,
|
|
22
|
+
store: req.session,
|
|
23
|
+
siteId: req.params.siteId,
|
|
24
|
+
pageUrl: req.params.pageUrl,
|
|
25
|
+
elementName: req.body?.elementName,
|
|
26
|
+
file: req.file
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (result.status !== 200) {
|
|
30
|
+
logger.error("Upload failed", result);
|
|
31
|
+
return res.status(result.status).json(errorResponse(result.status, result.errorMessage || 'File upload failed'));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return res.json(successResponse(result.data));
|
|
35
|
+
}
|
|
36
|
+
];
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
// 🔍 Select all file inputs that have the .govcy-file-upload class
|
|
2
|
+
const fileInputs = document.querySelectorAll('input[type="file"].govcy-file-upload');
|
|
3
|
+
|
|
4
|
+
// 🔁 Loop over each file input and attach a change event listener
|
|
5
|
+
fileInputs.forEach(input => {
|
|
6
|
+
input.addEventListener('change', async (event) => {
|
|
7
|
+
const messages = {
|
|
8
|
+
"uploadSuccesful": {
|
|
9
|
+
"el": "Το αρχείο ανεβαστηκε",
|
|
10
|
+
"en": "File uploaded successfully",
|
|
11
|
+
"tr": "File uploaded successfully"
|
|
12
|
+
},
|
|
13
|
+
"uploadFailed": {
|
|
14
|
+
"el": "Αποτυχια ανεβασης",
|
|
15
|
+
"en": "File upload failed",
|
|
16
|
+
"tr": "File upload failed"
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
// 🔐 Get the CSRF token from a hidden input field (generated by your backend)
|
|
20
|
+
const csrfToken = document.querySelector('input[type="hidden"][name="_csrf"]')?.value;
|
|
21
|
+
// 🔧 Define siteId and pageUrl (you can dynamically extract these later)
|
|
22
|
+
const siteId = window._govcySiteId || "";
|
|
23
|
+
const pageUrl = window._govcyPageUrl || "";
|
|
24
|
+
const lang = window._govcyLang || "el";
|
|
25
|
+
// 📦 Grab the selected file
|
|
26
|
+
const file = event.target.files[0];
|
|
27
|
+
const elementName = input.name; // Form field's `name` attribute
|
|
28
|
+
|
|
29
|
+
if (!file) return; // Exit if no file was selected
|
|
30
|
+
|
|
31
|
+
// 🧵 Prepare form-data payload for the API
|
|
32
|
+
const formData = new FormData();
|
|
33
|
+
formData.append('file', file); // Attach the actual file
|
|
34
|
+
formData.append('elementName', elementName); // Attach the field name for backend lookup
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// 🚀 Send file to the backend upload API
|
|
38
|
+
const response = await axios.post(`/apis/${siteId}/${pageUrl}/upload`, formData, {
|
|
39
|
+
headers: {
|
|
40
|
+
'X-CSRF-Token': csrfToken // 🔐 Pass CSRF token in custom header
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const { sha256, fileId } = response.data.Data;
|
|
45
|
+
|
|
46
|
+
// 📝 Store returned metadata in hidden fields for submission with the form
|
|
47
|
+
document.querySelector(`[name="${elementName}Attachment[fileId]"`).value = fileId;
|
|
48
|
+
document.querySelector(`[name="${elementName}Attachment[sha256]"`).value = sha256;
|
|
49
|
+
|
|
50
|
+
// ✅ Success
|
|
51
|
+
// Create an instance of GovcyFrontendRendererBrowser
|
|
52
|
+
const renderer = new GovcyFrontendRendererBrowser();
|
|
53
|
+
// Define the input data
|
|
54
|
+
const inputData =
|
|
55
|
+
{
|
|
56
|
+
"site": {
|
|
57
|
+
"lang": lang
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const fileInputMap = window._govcyFileInputs || {};
|
|
62
|
+
let fileElement = fileInputMap[elementName];
|
|
63
|
+
fileElement.element = "fileView";
|
|
64
|
+
fileElement.params.fileId = fileId;
|
|
65
|
+
fileElement.params.sha256 = sha256;
|
|
66
|
+
fileElement.params.visuallyHiddenText = fileElement.params.label;
|
|
67
|
+
fileElement.params.error = null;
|
|
68
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
69
|
+
fileElement.params.viewHref = "#viewHref";
|
|
70
|
+
fileElement.params.deleteHref = "#deleteHref";
|
|
71
|
+
// Construct the JSONTemplate
|
|
72
|
+
const JSONTemplate = {
|
|
73
|
+
"elements": [fileElement]
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
//render HTML into string
|
|
77
|
+
let renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
|
|
78
|
+
// look for element with id `${elementName}-outer-control`
|
|
79
|
+
// if not found look for element with id `${elementName}-input-control`
|
|
80
|
+
// if not found look for element with id `${elementName}-view-control`
|
|
81
|
+
var outerElement = document.getElementById(`${elementName}-outer-control`)
|
|
82
|
+
|| document.getElementById(`${elementName}-input-control`)
|
|
83
|
+
|| document.getElementById(`${elementName}-view-control`);
|
|
84
|
+
|
|
85
|
+
if (outerElement) {
|
|
86
|
+
//remove all classes from outerElement
|
|
87
|
+
outerElement.className = "";
|
|
88
|
+
//set the id of the outerElement to `${elementName}-outer-control`
|
|
89
|
+
outerElement.id = `${elementName}-outer-control`;
|
|
90
|
+
//update DOM and initialize the JS components
|
|
91
|
+
renderer.updateDOMAndInitialize(`${elementName}-outer-control`, renderedHtml);
|
|
92
|
+
}
|
|
93
|
+
// ✅ Update ARIA live region with success message
|
|
94
|
+
const statusRegion = document.getElementById('_govcy-upload-status');
|
|
95
|
+
if (statusRegion) {
|
|
96
|
+
statusRegion.textContent = messages.uploadSuccesful[lang];
|
|
97
|
+
setTimeout(() => {
|
|
98
|
+
statusRegion.textContent = '';
|
|
99
|
+
}, 10000);
|
|
100
|
+
}
|
|
101
|
+
// alert('✅ File uploaded successfully');
|
|
102
|
+
|
|
103
|
+
} catch (err) {
|
|
104
|
+
// Create an instance of GovcyFrontendRendererBrowser
|
|
105
|
+
const renderer = new GovcyFrontendRendererBrowser();
|
|
106
|
+
const lang = window._govcyLang || "el";
|
|
107
|
+
// Define the input data
|
|
108
|
+
const inputData =
|
|
109
|
+
{
|
|
110
|
+
"site": {
|
|
111
|
+
"lang": lang
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const fileInputMap = window._govcyFileInputs || {};
|
|
115
|
+
let fileElement = fileInputMap[elementName];
|
|
116
|
+
fileElement.element = "fileInput";
|
|
117
|
+
fileElement.params.fileId = "";
|
|
118
|
+
fileElement.params.sha256 = ""
|
|
119
|
+
fileElement.params.error = messages.uploadFailed;
|
|
120
|
+
|
|
121
|
+
// Construct the JSONTemplate
|
|
122
|
+
const JSONTemplate = {
|
|
123
|
+
"elements": [fileElement]
|
|
124
|
+
};
|
|
125
|
+
//render HTML into string
|
|
126
|
+
let renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
|
|
127
|
+
var outerElement = document.getElementById(`${elementName}-outer-control`)
|
|
128
|
+
|| document.getElementById(`${elementName}-input-control`)
|
|
129
|
+
|| document.getElementById(`${elementName}-view-control`);
|
|
130
|
+
|
|
131
|
+
if (outerElement) {
|
|
132
|
+
//remove all classes from outerElement
|
|
133
|
+
outerElement.className = "";
|
|
134
|
+
//set the id of the outerElement to `${elementName}-outer-control`
|
|
135
|
+
outerElement.id = `${elementName}-outer-control`;
|
|
136
|
+
//update DOM and initialize the JS components
|
|
137
|
+
renderer.updateDOMAndInitialize(`${elementName}-outer-control`, renderedHtml);
|
|
138
|
+
//TODO: Kamran need to figure a way to re register the DOM event on change
|
|
139
|
+
}
|
|
140
|
+
// ✅ Update ARIA live region with success message
|
|
141
|
+
const statusRegion = document.getElementById('_govcy-upload-error');
|
|
142
|
+
if (statusRegion) {
|
|
143
|
+
statusRegion.textContent = messages.uploadFailed[lang];
|
|
144
|
+
setTimeout(() => {
|
|
145
|
+
statusRegion.textContent = '';
|
|
146
|
+
}, 10000);
|
|
147
|
+
}
|
|
148
|
+
// // ⚠️ Show an error message if upload fails
|
|
149
|
+
// alert('❌ Upload failed: ' + (err.response?.data?.error || err.message));
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
});
|
|
@@ -113,9 +113,9 @@ export const staticResources = {
|
|
|
113
113
|
element: "htmlElement",
|
|
114
114
|
params: {
|
|
115
115
|
text: {
|
|
116
|
-
en: `<script src="/js/govcyForms.js"></script>`,
|
|
117
|
-
el: `<script src="/js/govcyForms.js"></script>`,
|
|
118
|
-
tr: `<script src="/js/govcyForms.js"></script>`
|
|
116
|
+
en: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
117
|
+
el: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`,
|
|
118
|
+
tr: `<script src="https://cdn.jsdelivr.net/npm/axios@1.6.2/dist/axios.min.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyCompiledTemplates.browser.js"></script><script src="https://cdn.jsdelivr.net/gh/gov-cy/govcy-frontend-renderer@v1/dist/govcyFrontendRenderer.browser.js"></script><script src="/js/govcyForms.js"></script><script src="/js/govcyFiles.js"></script>`
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
121
|
},
|
|
@@ -192,6 +192,27 @@ export function csrfTokenInput(csrfToken) {
|
|
|
192
192
|
};
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Get the site and page input elements
|
|
197
|
+
* @param {string} siteId The site id
|
|
198
|
+
* @param {string} pageUrl The page url
|
|
199
|
+
* @param {string} lang The page language
|
|
200
|
+
* @returns {object} htmlElement with the site and page inputs
|
|
201
|
+
*/
|
|
202
|
+
export function siteAndPageInput(siteId, pageUrl, lang = "el") {
|
|
203
|
+
const siteAndPageInputs = `<input type="hidden" name="_siteId" value="${siteId}"><input type="hidden" name="_pageUrl" value="${pageUrl}"><input type="hidden" name="_lang" value="${lang}">`;
|
|
204
|
+
return {
|
|
205
|
+
element: "htmlElement",
|
|
206
|
+
params: {
|
|
207
|
+
text: {
|
|
208
|
+
en: siteAndPageInputs,
|
|
209
|
+
el: siteAndPageInputs,
|
|
210
|
+
tr: siteAndPageInputs
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
195
216
|
/**
|
|
196
217
|
* Error page template
|
|
197
218
|
* @param {object} title the title text element
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines if a request is targeting an API endpoint.
|
|
3
|
+
* Currently matches:
|
|
4
|
+
* - Accept header with application/json
|
|
5
|
+
* - URLs ending with /upload or /download under a site/page structure
|
|
6
|
+
*
|
|
7
|
+
* @param {object} req - Express request object
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isApiRequest(req) {
|
|
11
|
+
const acceptJson = (req.headers?.accept || "").toLowerCase().includes("application/json");
|
|
12
|
+
|
|
13
|
+
const apiUrlPattern = /^\/apis\/[^/]+\/[^/]+\/(upload|download)$/;
|
|
14
|
+
const isStructuredApiUrl = apiUrlPattern.test(req.originalUrl || req.url);
|
|
15
|
+
|
|
16
|
+
return acceptJson || isStructuredApiUrl;
|
|
17
|
+
}
|
|
@@ -5,7 +5,7 @@ import { logger } from "./govcyLogger.mjs";
|
|
|
5
5
|
* Utility to handle API communication with retry logic
|
|
6
6
|
* @param {string} method - HTTP method (e.g., 'post', 'get', etc.)
|
|
7
7
|
* @param {string} url - API endpoint URL
|
|
8
|
-
* @param {object} inputData - Payload for the request (optional)
|
|
8
|
+
* @param {object|FormData} inputData - Payload for the request (optional)
|
|
9
9
|
* @param {boolean} useAccessTokenAuth - Whether to use Authorization header with Bearer token
|
|
10
10
|
* @param {object} user - User object containing access_token (optional)
|
|
11
11
|
* @param {object} headers - Custom headers (optional)
|
|
@@ -39,6 +39,14 @@ export async function govcyApiRequest(
|
|
|
39
39
|
requestHeaders['Authorization'] = `Bearer ${user.access_token}`;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// If inputData is FormData, for attachments
|
|
43
|
+
if (inputData instanceof (await import('form-data')).default) {
|
|
44
|
+
requestHeaders = {
|
|
45
|
+
...requestHeaders,
|
|
46
|
+
...inputData.getHeaders(), // includes boundary in content-type
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
42
50
|
while (attempt < retries) {
|
|
43
51
|
try {
|
|
44
52
|
logger.debug(`📤 Sending API request (Attempt ${attempt + 1})`, { method, url, inputData, requestHeaders });
|
|
@@ -47,13 +55,22 @@ export async function govcyApiRequest(
|
|
|
47
55
|
const axiosConfig = {
|
|
48
56
|
method,
|
|
49
57
|
url,
|
|
50
|
-
|
|
58
|
+
...(inputData instanceof (await import('form-data')).default // If inputData is FormData, for attachments
|
|
59
|
+
? { data: inputData }
|
|
60
|
+
: { [method?.toLowerCase() === 'get' ? 'params' : 'data']: inputData }),
|
|
51
61
|
headers: requestHeaders,
|
|
52
62
|
timeout: 10000, // 10 seconds timeout
|
|
53
63
|
// ✅ Treat only these statuses as "resolved" (no throw)
|
|
54
64
|
validateStatus: (status) => allowedHTTPStatusCodes.includes(status),
|
|
55
65
|
};
|
|
56
66
|
|
|
67
|
+
// If inputData is FormData, for attachments
|
|
68
|
+
if (inputData instanceof (await import('form-data')).default) {
|
|
69
|
+
axiosConfig.maxContentLength = Infinity;
|
|
70
|
+
axiosConfig.maxBodyLength = Infinity;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
57
74
|
// Add httpsAgent if NOT production to allow self-signed certificates
|
|
58
75
|
// Use per-call config for self-signed certs
|
|
59
76
|
if (allowSelfSignedCerts) {
|
|
@@ -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,22 @@
|
|
|
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";
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Helper function to populate form data with session data
|
|
12
13
|
* @param {Array} formElements The form elements
|
|
13
14
|
* @param {*} theData The data either from session or request
|
|
15
|
+
* @param {Object} validationErrors The validation errors
|
|
16
|
+
* @param {Object} store The session store
|
|
17
|
+
* @param {string} siteId The site ID
|
|
18
|
+
* @param {string} pageUrl The page URL
|
|
19
|
+
* @param {string} lang The language
|
|
14
20
|
*/
|
|
15
|
-
export function populateFormData(formElements, theData, validationErrors) {
|
|
21
|
+
export function populateFormData(formElements, theData, validationErrors, store = {}, siteId = "", pageUrl = "", lang = "el") {
|
|
16
22
|
const inputElements = ALLOWED_FORM_ELEMENTS;
|
|
17
|
-
|
|
23
|
+
let fileInputElements = {};
|
|
18
24
|
// Recursively populate form data with session data
|
|
19
25
|
formElements.forEach(element => {
|
|
20
26
|
if (inputElements.includes(element.element)) {
|
|
@@ -53,6 +59,24 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
53
59
|
// Invalid format (not matching D/M/YYYY or DD/MM/YYYY)
|
|
54
60
|
element.params.value = "";
|
|
55
61
|
}
|
|
62
|
+
} else if (element.element === "fileInput") {
|
|
63
|
+
// For fileInput, we change the element.element to "fileView" and set the
|
|
64
|
+
// fileId and sha256 from the session store
|
|
65
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, fieldName + "Attachment");
|
|
66
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
67
|
+
if (fileData) {
|
|
68
|
+
element.element = "fileView";
|
|
69
|
+
element.params.fileId = fileData.fileId;
|
|
70
|
+
element.params.sha256 = fileData.sha256;
|
|
71
|
+
element.params.visuallyHiddenText = element.params.label;
|
|
72
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
73
|
+
element.params.viewHref = "#viewHref";
|
|
74
|
+
element.params.deleteHref = "#deleteHref";
|
|
75
|
+
} else {
|
|
76
|
+
// TODO: Ask Andreas how to handle empty file inputs
|
|
77
|
+
element.params.value = "";
|
|
78
|
+
}
|
|
79
|
+
fileInputElements[fieldName] = element;
|
|
56
80
|
// Handle all other input elements (textInput, checkboxes, radios, etc.)
|
|
57
81
|
} else {
|
|
58
82
|
element.params.value = theData[fieldName] || "";
|
|
@@ -88,6 +112,29 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
88
112
|
});
|
|
89
113
|
}
|
|
90
114
|
});
|
|
115
|
+
// add file input elements's definition in js object
|
|
116
|
+
if (fileInputElements != {}) {
|
|
117
|
+
const scriptTag = `
|
|
118
|
+
<script type="text/javascript">
|
|
119
|
+
window._govcyFileInputs = ${JSON.stringify(fileInputElements)};
|
|
120
|
+
window._govcySiteId = "${siteId}";
|
|
121
|
+
window._govcyPageUrl = "${pageUrl}";
|
|
122
|
+
window._govcyLang = "${lang}";
|
|
123
|
+
</script>
|
|
124
|
+
<div id="_govcy-upload-status" class="govcy-visually-hidden" role="status" aria-live="polite"></div>
|
|
125
|
+
<div id="_govcy-upload-error" class="govcy-visually-hidden" role="alert" aria-live="assertive"></div>
|
|
126
|
+
`.trim();
|
|
127
|
+
formElements.push({
|
|
128
|
+
element: 'htmlElement',
|
|
129
|
+
params: {
|
|
130
|
+
text: {
|
|
131
|
+
en: scriptTag,
|
|
132
|
+
el: scriptTag,
|
|
133
|
+
tr: scriptTag
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
91
138
|
}
|
|
92
139
|
|
|
93
140
|
|
|
@@ -96,9 +143,12 @@ export function populateFormData(formElements, theData, validationErrors) {
|
|
|
96
143
|
*
|
|
97
144
|
* @param {Array} elements - The form elements (including conditional ones).
|
|
98
145
|
* @param {Object} formData - The submitted form data.
|
|
146
|
+
* @param {Object} store - The session store .
|
|
147
|
+
* @param {string} siteId - The site ID .
|
|
148
|
+
* @param {string} pageUrl - The page URL .
|
|
99
149
|
* @returns {Object} filteredData - The filtered form data.
|
|
100
150
|
*/
|
|
101
|
-
export function getFormData(elements, formData) {
|
|
151
|
+
export function getFormData(elements, formData, store = {}, siteId = "", pageUrl = "") {
|
|
102
152
|
const filteredData = {};
|
|
103
153
|
elements.forEach(element => {
|
|
104
154
|
const { name } = element.params || {};
|
|
@@ -130,6 +180,17 @@ export function getFormData(elements, formData) {
|
|
|
130
180
|
filteredData[`${name}_day`] = day !== undefined && day !== null ? day : "";
|
|
131
181
|
filteredData[`${name}_month`] = month !== undefined && month !== null ? month : "";
|
|
132
182
|
filteredData[`${name}_year`] = year !== undefined && year !== null ? year : "";
|
|
183
|
+
// handle fileInput
|
|
184
|
+
} else if (element.element === "fileInput") {
|
|
185
|
+
// fileInput elements are already stored in the store when it was uploaded
|
|
186
|
+
// so we just need to check if the file exists in the dataLayer in the store and add it the filteredData
|
|
187
|
+
const fileData = dataLayer.getFormDataValue(store, siteId, pageUrl, name + "Attachment");
|
|
188
|
+
if (fileData) {
|
|
189
|
+
filteredData[name + "Attachment"] = fileData;
|
|
190
|
+
} else {
|
|
191
|
+
//TODO: Ask Andreas how to handle empty file inputs
|
|
192
|
+
filteredData[name + "Attachment"] = ""; // or handle as needed
|
|
193
|
+
}
|
|
133
194
|
// Handle other elements (e.g., textInput, textArea, datePicker)
|
|
134
195
|
} else {
|
|
135
196
|
filteredData[name] = formData[name] !== undefined && formData[name] !== null ? formData[name] : "";
|
|
@@ -0,0 +1,282 @@
|
|
|
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
|
+
errorMessage: 'Missing file or element name'
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Get the upload configuration
|
|
35
|
+
const uploadCfg = service?.site?.fileUploadAPIEndpoint;
|
|
36
|
+
// Check if upload configuration is available
|
|
37
|
+
if (!uploadCfg?.url || !uploadCfg?.clientKey || !uploadCfg?.serviceId) {
|
|
38
|
+
return {
|
|
39
|
+
status: 400,
|
|
40
|
+
errorMessage: 'Missing upload configuration'
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Environment vars
|
|
45
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
46
|
+
let url = getEnvVariable(uploadCfg.url || "", false);
|
|
47
|
+
const clientKey = getEnvVariable(uploadCfg.clientKey || "", false);
|
|
48
|
+
const serviceId = getEnvVariable(uploadCfg.serviceId || "", false);
|
|
49
|
+
const dsfGtwKey = getEnvVariable(uploadCfg?.dsfgtwApiKey || "", "");
|
|
50
|
+
const method = (uploadCfg?.method || "PUT").toLowerCase();
|
|
51
|
+
|
|
52
|
+
// Check if the upload API is configured correctly
|
|
53
|
+
if (!url || !clientKey) {
|
|
54
|
+
return {
|
|
55
|
+
status: 400,
|
|
56
|
+
errorMessage: 'Missing environment variables for upload'
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Construct the URL with tag being the elementName
|
|
61
|
+
const tag = encodeURIComponent(elementName.trim());
|
|
62
|
+
url += `/${tag}`;
|
|
63
|
+
|
|
64
|
+
// Get the page configuration using utility (safely extracts the correct page)
|
|
65
|
+
const page = getPageConfigData(service, pageUrl);
|
|
66
|
+
// Check if the page template is valid
|
|
67
|
+
if (!page?.pageTemplate) {
|
|
68
|
+
return {
|
|
69
|
+
status: 400,
|
|
70
|
+
errorMessage: 'Invalid page configuration'
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ----- Conditional logic comes here
|
|
75
|
+
// Respect conditional logic: If the page is skipped due to conditions, abort
|
|
76
|
+
const conditionResult = evaluatePageConditions(page, store, siteId);
|
|
77
|
+
if (conditionResult.result === false) {
|
|
78
|
+
return {
|
|
79
|
+
status: 403,
|
|
80
|
+
errorMessage: 'This page is skipped by conditional logic'
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// deep copy the page template to avoid modifying the original
|
|
85
|
+
const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
|
|
86
|
+
// Validate the field: Only allow upload if the page contains a fileInput with the given name
|
|
87
|
+
const isAllowed = pageContainsFileInput(pageTemplateCopy, elementName);
|
|
88
|
+
if (!isAllowed) {
|
|
89
|
+
return {
|
|
90
|
+
status: 403,
|
|
91
|
+
errorMessage: `File input [${elementName}] not allowed on this page`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Empty file check
|
|
96
|
+
if (file.size === 0) {
|
|
97
|
+
return {
|
|
98
|
+
status: 400,
|
|
99
|
+
errorMessage: 'Uploaded file is empty'
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// file type checks
|
|
104
|
+
// 1. Check declared mimetype
|
|
105
|
+
if (!ALLOWED_FILE_MIME_TYPES.includes(file.mimetype)) {
|
|
106
|
+
return {
|
|
107
|
+
status: 400,
|
|
108
|
+
errorMessage: 'Invalid file type (MIME not allowed)'
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Check actual file content (magic bytes) matches claimed MIME type
|
|
113
|
+
if (!isMagicByteValid(file.buffer, file.mimetype)) {
|
|
114
|
+
return {
|
|
115
|
+
status: 400,
|
|
116
|
+
errorMessage: 'Invalid file type (magic byte mismatch)'
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// File size check
|
|
121
|
+
if (file.size > ALLOWED_FILE_SIZE_MB * 1024 * 1024) {
|
|
122
|
+
return {
|
|
123
|
+
status: 400,
|
|
124
|
+
errorMessage: 'File exceeds allowed size'
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Prepare FormData
|
|
129
|
+
const form = new FormData();
|
|
130
|
+
form.append('file', file.buffer, {
|
|
131
|
+
filename: file.originalname,
|
|
132
|
+
contentType: file.mimetype,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
logger.debug("Prepared FormData with file:", {
|
|
136
|
+
filename: file.originalname,
|
|
137
|
+
mimetype: file.mimetype,
|
|
138
|
+
size: file.size
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// Get the user
|
|
142
|
+
const user = dataLayer.getUser(store);
|
|
143
|
+
// Perform the upload request
|
|
144
|
+
const response = await govcyApiRequest(
|
|
145
|
+
method,
|
|
146
|
+
url,
|
|
147
|
+
form,
|
|
148
|
+
true,
|
|
149
|
+
user,
|
|
150
|
+
{
|
|
151
|
+
accept: "text/plain",
|
|
152
|
+
"client-key": clientKey,
|
|
153
|
+
"service-id": serviceId,
|
|
154
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
|
|
155
|
+
},
|
|
156
|
+
3,
|
|
157
|
+
allowSelfSignedCerts
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// If not succeeded, handle error
|
|
161
|
+
if (!response?.Succeeded) {
|
|
162
|
+
return {
|
|
163
|
+
status: 500,
|
|
164
|
+
errorMessage: `${response?.ErrorCode} - ${response?.ErrorMessage} - fileUploadAPIEndpoint returned succeeded false`
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Check if the response contains the expected data
|
|
169
|
+
if (!response?.Data?.fileId || !response?.Data?.sha256) {
|
|
170
|
+
return {
|
|
171
|
+
status: 500,
|
|
172
|
+
errorMessage: 'Missing fileId or sha256 in response'
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// ✅ Success
|
|
177
|
+
// Store the file metadata in the session store
|
|
178
|
+
dataLayer.storePageDataElement(store, siteId, pageUrl, elementName+"Attachment", {
|
|
179
|
+
sha256: response.Data.sha256,
|
|
180
|
+
fileId: response.Data.fileId,
|
|
181
|
+
});
|
|
182
|
+
logger.debug("File upload successful", response.Data);
|
|
183
|
+
logger.info(`File uploaded successfully for element ${elementName} on page ${pageUrl} for site ${siteId}`);
|
|
184
|
+
return {
|
|
185
|
+
status: 200,
|
|
186
|
+
data: {
|
|
187
|
+
sha: response.Data.sha256,
|
|
188
|
+
filename: response.Data.fileName || '',
|
|
189
|
+
fileId: response.Data.fileId,
|
|
190
|
+
mimeType: response.Data.contentType || '',
|
|
191
|
+
sha256: response.Data.fileSize || ''
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return {
|
|
197
|
+
status: 500,
|
|
198
|
+
errorMessage: 'Upload failed' + (err.message ? `: ${err.message}` : ''),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
//--------------------------------------------------------------------------
|
|
204
|
+
// Helper Functions
|
|
205
|
+
/**
|
|
206
|
+
* Recursively checks whether any element (or its children) is a fileInput
|
|
207
|
+
* with the matching elementName.
|
|
208
|
+
*
|
|
209
|
+
* Supports:
|
|
210
|
+
* - Top-level fileInput
|
|
211
|
+
* - Nested `params.elements` (used in groups, conditionals, etc.)
|
|
212
|
+
* - Conditional radios/checkboxes with `items[].conditionalElements`
|
|
213
|
+
*
|
|
214
|
+
* @param {Array} elements - The array of elements to search
|
|
215
|
+
* @param {string} targetName - The name of the file input to check
|
|
216
|
+
* @returns {boolean} True if a matching fileInput is found, false otherwise
|
|
217
|
+
*/
|
|
218
|
+
function containsFileInput(elements = [], targetName) {
|
|
219
|
+
for (const el of elements) {
|
|
220
|
+
// ✅ Direct file input match
|
|
221
|
+
if (el.element === 'fileInput' && el.params?.name === targetName) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
// 🔁 Recurse into nested elements (e.g. groups, conditionals)
|
|
225
|
+
if (Array.isArray(el?.params?.elements)) {
|
|
226
|
+
if (containsFileInput(el.params.elements, targetName)) return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 🎯 Special case: conditional radios/checkboxes
|
|
230
|
+
if (
|
|
231
|
+
(el.element === 'radios' || el.element === 'checkboxes') &&
|
|
232
|
+
Array.isArray(el?.params?.items)
|
|
233
|
+
) {
|
|
234
|
+
for (const item of el.params.items) {
|
|
235
|
+
if (Array.isArray(item?.conditionalElements)) {
|
|
236
|
+
if (containsFileInput(item.conditionalElements, targetName)) return true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Checks whether the specified page contains a valid fileInput for this element ID
|
|
246
|
+
* under any <form> element in its sections
|
|
247
|
+
*
|
|
248
|
+
* @param {object} pageTemplate The page template object
|
|
249
|
+
* @param {string} elementName The name of the element to check
|
|
250
|
+
* @return {boolean} True if a fileInput exists, false otherwise
|
|
251
|
+
*/
|
|
252
|
+
function pageContainsFileInput(pageTemplate, elementName) {
|
|
253
|
+
const sections = pageTemplate?.sections || [];
|
|
254
|
+
return sections.some(section =>
|
|
255
|
+
section?.elements?.some(el =>
|
|
256
|
+
el.element === 'form' &&
|
|
257
|
+
containsFileInput(el.params?.elements, elementName)
|
|
258
|
+
)
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Validates magic bytes against expected mimetype
|
|
264
|
+
* @param {Buffer} buffer
|
|
265
|
+
* @param {string} mimetype
|
|
266
|
+
* @returns {boolean}
|
|
267
|
+
*/
|
|
268
|
+
function isMagicByteValid(buffer, mimetype) {
|
|
269
|
+
const signatures = {
|
|
270
|
+
'application/pdf': [0x25, 0x50, 0x44, 0x46], // %PDF
|
|
271
|
+
'image/png': [0x89, 0x50, 0x4E, 0x47], // PNG
|
|
272
|
+
'image/jpeg': [0xFF, 0xD8, 0xFF], // JPG/JPEG
|
|
273
|
+
'application/vnd.openxmlformats-officedocument.wordprocessingml.document': [0x50, 0x4B, 0x03, 0x04], // DOCX
|
|
274
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': [0x50, 0x4B, 0x03, 0x04], // XLSX
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const expected = signatures[mimetype];
|
|
278
|
+
if (!expected) return false; // unknown type
|
|
279
|
+
|
|
280
|
+
const actual = Array.from(buffer.slice(0, expected.length));
|
|
281
|
+
return expected.every((byte, i) => actual[i] === byte);
|
|
282
|
+
}
|
|
@@ -263,6 +263,8 @@ 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
|
+
: (field.element === "fileInput") // Handle fileInput
|
|
267
|
+
? formData[`${field.params.name}Attachment`] || ""
|
|
266
268
|
: formData[field.params.name] || ""; // Get submitted value
|
|
267
269
|
|
|
268
270
|
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items
|
|
@@ -310,6 +312,8 @@ export function validateFormElements(elements, formData, pageUrl) {
|
|
|
310
312
|
formData[`${conditionalElement.params.name}_day`]]
|
|
311
313
|
.filter(Boolean) // Remove empty values
|
|
312
314
|
.join("-") // Join remaining parts
|
|
315
|
+
: (conditionalElement.element === "fileInput") // Handle fileInput
|
|
316
|
+
? formData[`${conditionalElement.params.name}Attachment`] || ""
|
|
313
317
|
: formData[conditionalElement.params.name] || ""; // Get submitted value
|
|
314
318
|
|
|
315
319
|
//Autocheck: check for "checkboxes", "radios", "select" if `fieldValue` is one of the `field.params.items`
|