@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,161 @@
|
|
|
1
|
+
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
2
|
+
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
3
|
+
import { getEnvVariable, getEnvVariableBool } from "../utils/govcyEnvVariables.mjs";
|
|
4
|
+
import { ALLOWED_FILE_MIME_TYPES } from "../utils/govcyConstants.mjs";
|
|
5
|
+
import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
|
|
6
|
+
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
7
|
+
import { logger } from '../utils/govcyLogger.mjs';
|
|
8
|
+
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
9
|
+
import { isMagicByteValid, pageContainsFileInput } from "../utils/govcyHandleFiles.mjs";
|
|
10
|
+
|
|
11
|
+
export function govcyFileViewHandler() {
|
|
12
|
+
return async (req, res, next) => {
|
|
13
|
+
try {
|
|
14
|
+
const { siteId, pageUrl, elementName } = req.params;
|
|
15
|
+
|
|
16
|
+
// Create a deep copy of the service to avoid modifying the original
|
|
17
|
+
let serviceCopy = req.serviceData;
|
|
18
|
+
|
|
19
|
+
// Get the download file configuration
|
|
20
|
+
const downloadCfg = serviceCopy?.site?.fileDownloadAPIEndpoint;
|
|
21
|
+
// Check if download file configuration is available
|
|
22
|
+
if (!downloadCfg?.url || !downloadCfg?.clientKey || !downloadCfg?.serviceId) {
|
|
23
|
+
return handleMiddlewareError(`File download APU configuration not found`, 404, next);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Environment vars
|
|
27
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
28
|
+
let url = getEnvVariable(downloadCfg.url || "", false);
|
|
29
|
+
const clientKey = getEnvVariable(downloadCfg.clientKey || "", false);
|
|
30
|
+
const serviceId = getEnvVariable(downloadCfg.serviceId || "", false);
|
|
31
|
+
const dsfGtwKey = getEnvVariable(downloadCfg?.dsfgtwApiKey || "", "");
|
|
32
|
+
const method = (downloadCfg?.method || "GET").toLowerCase();
|
|
33
|
+
|
|
34
|
+
// Check if the upload API is configured correctly
|
|
35
|
+
if (!url || !clientKey) {
|
|
36
|
+
return handleMiddlewareError(`Missing environment variables for upload`, 404, next);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
// ⤵️ Find the current page based on the URL
|
|
41
|
+
const page = getPageConfigData(serviceCopy, pageUrl);
|
|
42
|
+
|
|
43
|
+
// deep copy the page template to avoid modifying the original
|
|
44
|
+
const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
|
|
45
|
+
|
|
46
|
+
// ----- Conditional logic comes here
|
|
47
|
+
// ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
|
|
48
|
+
// const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
49
|
+
// if (conditionResult.result === false) {
|
|
50
|
+
// logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
|
|
51
|
+
// return handleMiddlewareError(`Page condition evaluated to true on POST — skipping form save and redirecting`, 404, next);
|
|
52
|
+
// }
|
|
53
|
+
|
|
54
|
+
// Validate the field: Only allow delete if the page contains a fileInput with the given name
|
|
55
|
+
const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
|
|
56
|
+
if (!fileInputElement) {
|
|
57
|
+
return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
//check the reference number
|
|
61
|
+
const referenceNo = dataLayer.getSiteLoadDataReferenceNumber(req.session, siteId);
|
|
62
|
+
if (!referenceNo) {
|
|
63
|
+
return handleMiddlewareError(`Missing submission reference number`, 404, next);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//get element data
|
|
67
|
+
const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
|
|
68
|
+
|
|
69
|
+
// If the element data is not found, return an error response
|
|
70
|
+
if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
|
|
71
|
+
return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Construct the URL with tag being the elementName
|
|
75
|
+
url += `/${encodeURIComponent(referenceNo)}/${encodeURIComponent(elementData.fileId)}/${encodeURIComponent(elementData.sha256)}`;
|
|
76
|
+
|
|
77
|
+
// Get the user
|
|
78
|
+
const user = dataLayer.getUser(req.session);
|
|
79
|
+
// Perform the upload request
|
|
80
|
+
const response = await govcyApiRequest(
|
|
81
|
+
method,
|
|
82
|
+
url,
|
|
83
|
+
{},
|
|
84
|
+
true,
|
|
85
|
+
user,
|
|
86
|
+
{
|
|
87
|
+
accept: "text/plain",
|
|
88
|
+
"client-key": clientKey,
|
|
89
|
+
"service-id": serviceId,
|
|
90
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
|
|
91
|
+
},
|
|
92
|
+
3,
|
|
93
|
+
allowSelfSignedCerts
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
// If not succeeded, handle error
|
|
97
|
+
if (!response?.Succeeded) {
|
|
98
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint returned succeeded false`, 500, next);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check if the response contains the expected data
|
|
102
|
+
if (!response?.Data?.contentType || !response?.Data?.fileName || !response?.Data?.base64) {
|
|
103
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - Missing contentType, fileName or base64 in response data`, 500, next);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Get filename
|
|
107
|
+
const filename = response?.Data?.fileName || "filename";
|
|
108
|
+
const fallbackFilename = asciiFallback(filename);
|
|
109
|
+
const utf8Filename = encodeRFC5987(filename);
|
|
110
|
+
|
|
111
|
+
// Decode base64 to binary
|
|
112
|
+
const fileBuffer = Buffer.from(response.Data.base64, 'base64');
|
|
113
|
+
|
|
114
|
+
// file type checks
|
|
115
|
+
// 1. Check declared mimetype
|
|
116
|
+
if (!ALLOWED_FILE_MIME_TYPES.includes(response?.Data?.contentType)) {
|
|
117
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (MIME not allowed)`, 500, next);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 2. Check actual file content (magic bytes) matches claimed MIME type
|
|
121
|
+
if (!isMagicByteValid(fileBuffer, response?.Data?.contentType)) {
|
|
122
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (magic byte mismatch)`, 500, next);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if Buffer is empty
|
|
126
|
+
if (!fileBuffer || fileBuffer.length === 0) {
|
|
127
|
+
return handleMiddlewareError(`fileDownloadAPIEndpoint - File is empty or invalid`, 500, next);
|
|
128
|
+
}
|
|
129
|
+
// Send the file to the browser
|
|
130
|
+
res.type(response?.Data?.contentType);
|
|
131
|
+
res.setHeader(
|
|
132
|
+
'Content-Disposition',
|
|
133
|
+
`inline; filename="${fallbackFilename}"; filename*=UTF-8''${utf8Filename}`
|
|
134
|
+
);
|
|
135
|
+
res.send(fileBuffer);
|
|
136
|
+
|
|
137
|
+
} catch (error) {
|
|
138
|
+
logger.error("Error in govcyViewFileHandler middleware:", error.message);
|
|
139
|
+
return next(error); // Pass the error to the next middleware
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
//------------------------------------------------------------------------------
|
|
145
|
+
// Helper functions
|
|
146
|
+
// rfc5987 encoding for filename*
|
|
147
|
+
function encodeRFC5987(str) {
|
|
148
|
+
return encodeURIComponent(str)
|
|
149
|
+
.replace(/['()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
|
|
150
|
+
.replace(/%(7C|60|5E)/g, (m, p) => '%' + p); // | ` ^
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ASCII fallback for old browsers
|
|
154
|
+
function asciiFallback(str) {
|
|
155
|
+
return str
|
|
156
|
+
.replace(/[^\x20-\x7E]/g, '') // strip non-ASCII
|
|
157
|
+
.replace(/[/\\?%*:|"<>]/g, '-') // reserved chars
|
|
158
|
+
.replace(/[\r\n]/g, ' ') // drop newlines
|
|
159
|
+
.replace(/"/g, "'") // no quotes
|
|
160
|
+
|| 'download';
|
|
161
|
+
}
|
|
@@ -6,6 +6,7 @@ import { logger } from "../utils/govcyLogger.mjs";
|
|
|
6
6
|
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
7
7
|
import { getFormData } from "../utils/govcyFormHandling.mjs"
|
|
8
8
|
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
9
|
+
import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs";
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
/**
|
|
@@ -42,7 +43,7 @@ export function govcyFormsPostHandler() {
|
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
// const formData = req.body; // Submitted data
|
|
45
|
-
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
|
|
46
47
|
|
|
47
48
|
// ☑️ Start validation from top-level form elements
|
|
48
49
|
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
@@ -59,7 +60,12 @@ export function govcyFormsPostHandler() {
|
|
|
59
60
|
|
|
60
61
|
//⤴️ Store validated form data in session
|
|
61
62
|
dataLayer.storePageData(req.session, siteId, pageUrl, formData);
|
|
62
|
-
|
|
63
|
+
|
|
64
|
+
// 🔄 Fire-and-forget temporary save (non-blocking)
|
|
65
|
+
(async () => {
|
|
66
|
+
try { await tempSaveIfConfigured(req.session, service, siteId); }
|
|
67
|
+
catch (e) { /* already logged internally */ }
|
|
68
|
+
})();
|
|
63
69
|
|
|
64
70
|
logger.debug("✅ Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req);
|
|
65
71
|
logger.info("✅ Form submitted successfully:", req.originalUrl);
|
|
@@ -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
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { logger } from "../utils/govcyLogger.mjs";
|
|
2
|
+
import { govcyLoadSubmissionDataAPIs } from "../utils/govcyLoadSubmissionDataAPIs.mjs";
|
|
3
|
+
/**
|
|
4
|
+
* Middleware to load submission data from APIs.
|
|
5
|
+
* This middleware fetches submission data from configured APIs and stores it in the session.
|
|
6
|
+
* @returns {function} Middleware function to load submission data from APIs
|
|
7
|
+
*/
|
|
8
|
+
export function govcyLoadSubmissionData() {
|
|
9
|
+
return async (req, res, next) => {
|
|
10
|
+
try {
|
|
11
|
+
const service = req.serviceData;
|
|
12
|
+
// Extract siteId from request
|
|
13
|
+
const { siteId } = req.params;
|
|
14
|
+
|
|
15
|
+
return await govcyLoadSubmissionDataAPIs(req.session, service, siteId, next);
|
|
16
|
+
|
|
17
|
+
} catch (error) {
|
|
18
|
+
logger.error("Error in govcyLoadSubmissionData middleware:", error.message);
|
|
19
|
+
return next(error); // Pass the error to the next middleware
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -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, null, req.query.route);
|
|
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));
|
|
@@ -117,3 +120,4 @@ export function govcyPageHandler() {
|
|
|
117
120
|
}
|
|
118
121
|
};
|
|
119
122
|
}
|
|
123
|
+
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// 🔍 Select all file inputs that have the .govcy-file-upload class
|
|
2
|
+
var 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(function(input) {
|
|
6
|
+
input.addEventListener('change', _uploadFileEventHandler);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Handles the upload of a file event
|
|
12
|
+
*
|
|
13
|
+
* @param {object} event The event
|
|
14
|
+
*/
|
|
15
|
+
function _uploadFileEventHandler(event) {
|
|
16
|
+
var input = event.target;
|
|
17
|
+
var messages = {
|
|
18
|
+
"uploadSuccesful": {
|
|
19
|
+
"el": "Το αρχείο ανεβαστηκε",
|
|
20
|
+
"en": "File uploaded successfully",
|
|
21
|
+
"tr": "File uploaded successfully"
|
|
22
|
+
},
|
|
23
|
+
"uploadFailed": {
|
|
24
|
+
"el": "Αποτυχια ανεβασης",
|
|
25
|
+
"en": "File upload failed",
|
|
26
|
+
"tr": "File upload failed"
|
|
27
|
+
},
|
|
28
|
+
"uploadFailed406": {
|
|
29
|
+
"el": "Το επιλεγμένο αρχείο είναι κενό",
|
|
30
|
+
"en": "The selected file is empty",
|
|
31
|
+
"tr": "The selected file is empty"
|
|
32
|
+
},
|
|
33
|
+
"uploadFailed407": {
|
|
34
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
35
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
36
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
37
|
+
},
|
|
38
|
+
"uploadFailed408": {
|
|
39
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι JPG, JPEG, PNG ή PDF",
|
|
40
|
+
"en": "The selected file must be a JPG, JPEG, PNG or PDF",
|
|
41
|
+
"tr": "The selected file must be a JPG, JPEG, PNG or PDF"
|
|
42
|
+
},
|
|
43
|
+
"uploadFailed409": {
|
|
44
|
+
"el": "Το επιλεγμένο αρχείο πρέπει να είναι μικρότερο από 5MB",
|
|
45
|
+
"en": "The selected file must be smaller than 5MB",
|
|
46
|
+
"tr": "The selected file must be smaller than 5MB"
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// 🔐 Get the CSRF token from a hidden input field (generated by your backend)
|
|
51
|
+
var csrfEl = document.querySelector('input[type="hidden"][name="_csrf"]');
|
|
52
|
+
var csrfToken = csrfEl ? csrfEl.value : '';
|
|
53
|
+
|
|
54
|
+
// 🔧 Define siteId and pageUrl (you can dynamically extract these later)
|
|
55
|
+
var siteId = window._govcySiteId || "";
|
|
56
|
+
var pageUrl = window._govcyPageUrl || "";
|
|
57
|
+
var lang = window._govcyLang || "el";
|
|
58
|
+
|
|
59
|
+
// 📦 Grab the selected file
|
|
60
|
+
var file = event.target.files[0];
|
|
61
|
+
var elementName = input.name; // Form field's `name` attribute
|
|
62
|
+
var elementId = input.id; // Form field's `id` attribute
|
|
63
|
+
|
|
64
|
+
if (!file) return; // Exit if no file was selected
|
|
65
|
+
|
|
66
|
+
// 🧵 Prepare form-data payload for the API
|
|
67
|
+
var formData = new FormData();
|
|
68
|
+
formData.append('file', file); // Attach the actual file
|
|
69
|
+
formData.append('elementName', elementName); // Attach the field name for backend lookup
|
|
70
|
+
|
|
71
|
+
// 🚀 CHANGED: using fetch instead of axios.post
|
|
72
|
+
fetch(`/apis/${siteId}/${pageUrl}/upload`, {
|
|
73
|
+
method: "POST",
|
|
74
|
+
headers: {
|
|
75
|
+
"X-CSRF-Token": csrfToken // 🔐 Pass CSRF token in custom header
|
|
76
|
+
},
|
|
77
|
+
body: formData
|
|
78
|
+
})
|
|
79
|
+
.then(function(response) {
|
|
80
|
+
// 🚀 CHANGED: fetch does not auto-throw on error codes → check manually
|
|
81
|
+
if (!response.ok) {
|
|
82
|
+
return response.json().then(function(errData) {
|
|
83
|
+
throw { response: { data: errData } };
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return response.json();
|
|
87
|
+
})
|
|
88
|
+
.then(function(data) {
|
|
89
|
+
// ✅ Success response
|
|
90
|
+
var sha256 = data.Data.sha256;
|
|
91
|
+
var fileId = data.Data.fileId;
|
|
92
|
+
|
|
93
|
+
// 📝 Store returned metadata in hidden fields if needed
|
|
94
|
+
// document.querySelector('[name="' + elementName + 'Attachment[fileId]"]').value = fileId;
|
|
95
|
+
// document.querySelector('[name="' + elementName + 'Attachment[sha256]"]').value = sha256;
|
|
96
|
+
|
|
97
|
+
// Render the file view
|
|
98
|
+
_renderFileElement("fileView", elementId, elementName, fileId, sha256, null);
|
|
99
|
+
|
|
100
|
+
// Accessibility: Update ARIA live region with success message
|
|
101
|
+
var statusRegion = document.getElementById('_govcy-upload-status');
|
|
102
|
+
if (statusRegion) {
|
|
103
|
+
setTimeout(function() {
|
|
104
|
+
statusRegion.textContent = messages.uploadSuccesful[lang];
|
|
105
|
+
}, 200);
|
|
106
|
+
setTimeout(function() {
|
|
107
|
+
statusRegion.textContent = '';
|
|
108
|
+
}, 5000);
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
.catch(function(err) {
|
|
112
|
+
// ⚠️ Show an error message if upload fails
|
|
113
|
+
var errorMessage = messages.uploadFailed;
|
|
114
|
+
var errorCode = err && err.response && err.response.data && err.response.data.ErrorCode;
|
|
115
|
+
|
|
116
|
+
if (errorCode === 406 || errorCode === 407 || errorCode === 408 || errorCode === 409) {
|
|
117
|
+
errorMessage = messages["uploadFailed" + errorCode];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Render the file input with error
|
|
121
|
+
_renderFileElement("fileInput", elementId, elementName, "", "", errorMessage);
|
|
122
|
+
|
|
123
|
+
// Re-bind the file input's change handler
|
|
124
|
+
var newInput = document.getElementById(elementId);
|
|
125
|
+
if (newInput) {
|
|
126
|
+
newInput.addEventListener('change', _uploadFileEventHandler);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Accessibility: Focus on the form field
|
|
130
|
+
document.getElementById(elementId)?.focus();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Renders a file element in the DOM
|
|
137
|
+
*
|
|
138
|
+
* @param {string} elementState The element state. Can be "fileInput" or "fileView"
|
|
139
|
+
* @param {string} elementId The element id
|
|
140
|
+
* @param {string} elementName The element name
|
|
141
|
+
* @param {string} fileId The file id
|
|
142
|
+
* @param {string} sha256 The sha256
|
|
143
|
+
* @param {object} errorMessage The error message in all supported languages
|
|
144
|
+
*/
|
|
145
|
+
function _renderFileElement(elementState, elementId, elementName, fileId, sha256, errorMessage) {
|
|
146
|
+
|
|
147
|
+
// Grab the query string part (?foo=bar&route=something)
|
|
148
|
+
var queryString = window.location.search;
|
|
149
|
+
// Parse it
|
|
150
|
+
var params = new URLSearchParams(queryString);
|
|
151
|
+
// Get the "route" value (null if not present)
|
|
152
|
+
var route = params.get("route");
|
|
153
|
+
|
|
154
|
+
// Create an instance of GovcyFrontendRendererBrowser
|
|
155
|
+
var renderer = new GovcyFrontendRendererBrowser();
|
|
156
|
+
var lang = window._govcyLang || "el";
|
|
157
|
+
// Define the input data
|
|
158
|
+
var inputData =
|
|
159
|
+
{
|
|
160
|
+
"site": {
|
|
161
|
+
"lang": lang
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
var fileInputMap = JSON.parse(JSON.stringify(window._govcyFileInputs));
|
|
165
|
+
var fileElement = fileInputMap[elementName];
|
|
166
|
+
fileElement.element = elementState;
|
|
167
|
+
if (errorMessage != null) fileElement.params.error = errorMessage;
|
|
168
|
+
if (fileId != null) fileElement.params.fileId = fileId;
|
|
169
|
+
if (sha256 != null) fileElement.params.sha256 = sha256;
|
|
170
|
+
if (elementState == "fileView") {
|
|
171
|
+
fileElement.params.visuallyHiddenText = fileElement.params.label;
|
|
172
|
+
// TODO: Also need to set the `view` and `download` URLs
|
|
173
|
+
fileElement.params.viewHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/view-file/" + elementName;
|
|
174
|
+
fileElement.params.viewTarget = "_blank";
|
|
175
|
+
fileElement.params.deleteHref = "/" + window._govcySiteId + "/" + window._govcyPageUrl + "/delete-file/" + elementName
|
|
176
|
+
+ (route !== null ? "?route=" + encodeURIComponent(route) : "");
|
|
177
|
+
}
|
|
178
|
+
// Construct the JSONTemplate
|
|
179
|
+
var JSONTemplate = {
|
|
180
|
+
"elements": [fileElement]
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
//render HTML into string
|
|
184
|
+
var renderedHtml = renderer.renderFromJSON(JSONTemplate,inputData);
|
|
185
|
+
var outerElement = document.getElementById(`${elementId}-outer-control`)
|
|
186
|
+
|| document.getElementById(`${elementId}-input-control`)
|
|
187
|
+
|| document.getElementById(`${elementId}-view-control`);
|
|
188
|
+
|
|
189
|
+
if (outerElement) {
|
|
190
|
+
//remove all classes from outerElement
|
|
191
|
+
outerElement.className = "";
|
|
192
|
+
//set the id of the outerElement to `${elementId}-outer-control`
|
|
193
|
+
outerElement.id = `${elementId}-outer-control`;
|
|
194
|
+
//update DOM and initialize the JS components
|
|
195
|
+
renderer.updateDOMAndInitialize(`${elementId}-outer-control`, renderedHtml);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
document.addEventListener("DOMContentLoaded", function () {
|
|
2
2
|
// --- Show conditionals for checked radios ---
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
// CHANGED: NodeList.prototype.forEach is not in IE11 → use Array.prototype.forEach.call
|
|
4
|
+
// CHANGED: Arrow function → function
|
|
5
|
+
Array.prototype.forEach.call(
|
|
6
|
+
document.querySelectorAll('.govcy-radio-input[data-aria-controls]:checked'),
|
|
7
|
+
function (radio) { // CHANGED: arrow → function
|
|
8
|
+
// CHANGED: const → var (ES5)
|
|
9
|
+
var targetId = radio.getAttribute('data-aria-controls');
|
|
10
|
+
var targetElement = document.getElementById(targetId);
|
|
6
11
|
|
|
7
|
-
|
|
8
|
-
|
|
12
|
+
if (targetElement) {
|
|
13
|
+
targetElement.classList.remove('govcy-radio__conditional--hidden');
|
|
14
|
+
}
|
|
9
15
|
}
|
|
10
|
-
|
|
16
|
+
);
|
|
17
|
+
|
|
11
18
|
// --- Disable submit button after form submission ---
|
|
12
|
-
|
|
19
|
+
// CHANGED: NodeList.forEach → Array.prototype.forEach.call
|
|
20
|
+
Array.prototype.forEach.call(document.querySelectorAll('form'), function (form) { // CHANGED
|
|
13
21
|
form.addEventListener('submit', function (e) {
|
|
14
|
-
const
|
|
22
|
+
// CHANGED: const → var (ES5)
|
|
23
|
+
var submitButton = form.querySelector('[type="submit"]');
|
|
15
24
|
if (submitButton) {
|
|
16
25
|
submitButton.disabled = true;
|
|
17
26
|
submitButton.setAttribute('aria-disabled', 'true');
|
|
27
|
+
// (Optional) announce busy state for AT:
|
|
28
|
+
// submitButton.setAttribute('aria-busy', 'true'); // CHANGED: optional a11y improvement
|
|
18
29
|
}
|
|
19
30
|
});
|
|
20
31
|
});
|
|
@@ -6,6 +6,11 @@ export const staticResources = {
|
|
|
6
6
|
el: "Υποβολή",
|
|
7
7
|
tr: "Gönder"
|
|
8
8
|
},
|
|
9
|
+
continue: {
|
|
10
|
+
en: "Continue",
|
|
11
|
+
el: "Συνέχεια",
|
|
12
|
+
tr: "Continue"
|
|
13
|
+
},
|
|
9
14
|
cancel: {
|
|
10
15
|
en: "Cancel",
|
|
11
16
|
el: "Ακύρωση",
|
|
@@ -100,6 +105,46 @@ export const staticResources = {
|
|
|
100
105
|
en: "We have received your request. ",
|
|
101
106
|
el: "Έχουμε λάβει την αίτησή σας. ",
|
|
102
107
|
tr: "We have received your request. "
|
|
108
|
+
},
|
|
109
|
+
fileUploaded : {
|
|
110
|
+
en: "File uploaded",
|
|
111
|
+
el: "Το αρχείο ανεβάστηκε",
|
|
112
|
+
tr: "File uploaded"
|
|
113
|
+
},
|
|
114
|
+
fileNotUploaded : {
|
|
115
|
+
en: "File has not been uploaded. ",
|
|
116
|
+
el: "Το αρχείο δεν ανεβάστηκε. ",
|
|
117
|
+
tr: "File has not been uploaded. "
|
|
118
|
+
},
|
|
119
|
+
fileYouHaveUploaded : {
|
|
120
|
+
en: "You have uploaded the file for \"{{file}}\"",
|
|
121
|
+
el: "Έχετε ανεβάσει το αρχείο \"{{file}}\"",
|
|
122
|
+
tr: "You have uploaded the file for \"{{file}}\""
|
|
123
|
+
},
|
|
124
|
+
deleteFileTitle : {
|
|
125
|
+
en: "Are you sure you want to delete the file \"{{file}}\"? ",
|
|
126
|
+
el: "Είστε σίγουροι ότι θέλετε να διαγράψετε το αρχείο \"{{file}}\";",
|
|
127
|
+
tr: "Are you sure you want to delete the file \"{{file}}\"? "
|
|
128
|
+
},
|
|
129
|
+
deleteYesOption: {
|
|
130
|
+
el:"Ναι, θέλω να διαγράψω το αρχείο",
|
|
131
|
+
en:"Yes, I want to delete this file",
|
|
132
|
+
tr:"Yes, I want to delete this file"
|
|
133
|
+
},
|
|
134
|
+
deleteNoOption: {
|
|
135
|
+
el:"Όχι, δεν θέλω να διαγράψω το αρχείο",
|
|
136
|
+
en:"No, I don't want to delete this file",
|
|
137
|
+
tr:"No, I don't want to delete this file"
|
|
138
|
+
},
|
|
139
|
+
deleteFileValidationError: {
|
|
140
|
+
en: "Select if you want to delete the file",
|
|
141
|
+
el: "Επιλέξτε αν θέλετε να διαγράψετε το αρχείο",
|
|
142
|
+
tr: "Select if you want to delete the file"
|
|
143
|
+
},
|
|
144
|
+
viewFile: {
|
|
145
|
+
en: "View file",
|
|
146
|
+
el: "Προβολή αρχείου",
|
|
147
|
+
tr: "View file"
|
|
103
148
|
}
|
|
104
149
|
},
|
|
105
150
|
//remderer sections
|
|
@@ -113,9 +158,9 @@ export const staticResources = {
|
|
|
113
158
|
element: "htmlElement",
|
|
114
159
|
params: {
|
|
115
160
|
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>`
|
|
161
|
+
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 type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
|
|
162
|
+
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 type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`,
|
|
163
|
+
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 type="module" src="/js/govcyForms.js"></script><script type="module" src="/js/govcyFiles.js"></script>`
|
|
119
164
|
}
|
|
120
165
|
}
|
|
121
166
|
},
|
|
@@ -192,6 +237,27 @@ export function csrfTokenInput(csrfToken) {
|
|
|
192
237
|
};
|
|
193
238
|
}
|
|
194
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Get the site and page input elements
|
|
242
|
+
* @param {string} siteId The site id
|
|
243
|
+
* @param {string} pageUrl The page url
|
|
244
|
+
* @param {string} lang The page language
|
|
245
|
+
* @returns {object} htmlElement with the site and page inputs
|
|
246
|
+
*/
|
|
247
|
+
export function siteAndPageInput(siteId, pageUrl, lang = "el") {
|
|
248
|
+
const siteAndPageInputs = `<input type="hidden" name="_siteId" value="${siteId}"><input type="hidden" name="_pageUrl" value="${pageUrl}"><input type="hidden" name="_lang" value="${lang}">`;
|
|
249
|
+
return {
|
|
250
|
+
element: "htmlElement",
|
|
251
|
+
params: {
|
|
252
|
+
text: {
|
|
253
|
+
en: siteAndPageInputs,
|
|
254
|
+
el: siteAndPageInputs,
|
|
255
|
+
tr: siteAndPageInputs
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
195
261
|
/**
|
|
196
262
|
* Error page template
|
|
197
263
|
* @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
|
+
}
|