@gov-cy/govcy-express-services 1.0.0-alpha.9 → 1.1.0

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.0.0-alpha.9",
3
+ "version": "1.1.0",
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",
@@ -44,11 +44,15 @@
44
44
  "test:integration": "mocha --recursive tests/integration/**/*.test.mjs",
45
45
  "test:package": "mocha --recursive tests/package/**/*.test.mjs",
46
46
  "test:functional": "mocha --timeout 30000 --recursive tests/functional/**/*.test.mjs",
47
- "test:watch": "mocha --watch --timeout 60000 tests/**/*.test.mjs"
47
+ "test:watch": "mocha --watch --timeout 60000 tests/**/*.test.mjs",
48
+ "coverage": "c8 --reporter=html --reporter=text --reporter=lcov --reporter=json-summary npm test",
49
+ "coverage:copy": "node -e \"try{require('fs').copyFileSync('coverage/coverage-summary.json','./coverage-summary.json')}catch(e){process.exit(0)}\"",
50
+ "coverage:report": "c8 report",
51
+ "coverage:badge": "coverage-badges --output ./coverage-badges.svg && npm run coverage:copy"
48
52
  },
49
53
  "dependencies": {
50
54
  "@gov-cy/dsf-email-templates": "^2.1.0",
51
- "@gov-cy/govcy-frontend-renderer": "^1.20.0",
55
+ "@gov-cy/govcy-frontend-renderer": "^1.24.0",
52
56
  "axios": "^1.9.0",
53
57
  "cookie-parser": "^1.4.7",
54
58
  "dotenv": "^16.3.1",
@@ -60,8 +64,10 @@
60
64
  "puppeteer": "^24.6.0"
61
65
  },
62
66
  "devDependencies": {
67
+ "c8": "^10.1.3",
63
68
  "chai": "^5.2.0",
64
69
  "chai-http": "^5.1.1",
70
+ "coverage-badges-cli": "^2.2.0",
65
71
  "mocha": "^11.1.0",
66
72
  "mochawesome": "^7.1.3",
67
73
  "nodemon": "^3.0.2",
@@ -2,7 +2,7 @@ import * as client from 'openid-client';
2
2
  import { getEnvVariable } from '../utils/govcyEnvVariables.mjs';
3
3
  import { logger } from "../utils/govcyLogger.mjs";
4
4
 
5
-
5
+ /* c8 ignore start */
6
6
  // OpenID Configuration
7
7
  const issuerUrl = getEnvVariable('CYLOGIN_ISSUER_URL');
8
8
  const clientId = getEnvVariable('CYLOGIN_CLIENT_ID');
@@ -131,3 +131,4 @@ export function getLogoutUrl(id_token_hint = '') {
131
131
 
132
132
  // Export config if needed elsewhere
133
133
  export { config };
134
+ /* c8 ignore end */
package/src/index.mjs CHANGED
@@ -24,8 +24,9 @@ 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';
28
- import { govcyDeleteFilePageHandler, govcyDeleteFilePostHandler } from './middleware/govcyDeleteFileHandler.mjs';
27
+ import { govcyFileUpload } from './middleware/govcyFileUpload.mjs';
28
+ import { govcyFileDeletePageHandler, govcyFileDeletePostHandler } from './middleware/govcyFileDeleteHandler.mjs';
29
+ import { govcyFileViewHandler } from './middleware/govcyFileViewHandler.mjs';
29
30
  import { isProdOrStaging , getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
30
31
  import { logger } from "./utils/govcyLogger.mjs";
31
32
 
@@ -137,7 +138,7 @@ export default function initializeGovCyExpressService(){
137
138
  requireAuth, // UNCOMMENT
138
139
  naturalPersonPolicy, // UNCOMMENT
139
140
  govcyServiceEligibilityHandler(true), // UNCOMMENT
140
- govcyUploadMiddleware);
141
+ govcyFileUpload);
141
142
 
142
143
  // 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
143
144
  app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyLoadSubmissionData(),govcyPageHandler(), renderGovcyPage());
@@ -151,14 +152,17 @@ export default function initializeGovCyExpressService(){
151
152
  // ✅ -- ROUTE: Add Success Page Route (BEFORE the dynamic route)
152
153
  app.get('/:siteId/success',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
153
154
 
155
+ // 👀🗃️ -- ROUTE: View file (BEFORE the dynamic route)
156
+ app.get('/:siteId/:pageUrl/view-file/:elementName', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileViewHandler());
157
+
154
158
  // ❌🗃️ -- ROUTE: Delete file (BEFORE the dynamic route)
155
- app.get('/:siteId/:pageUrl/:elementName/delete-file', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyDeleteFilePageHandler(), renderGovcyPage());
159
+ app.get('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileDeletePageHandler(), renderGovcyPage());
156
160
 
157
161
  // 📝 -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
158
162
  app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
159
163
 
160
164
  // ❌🗃️📥 -- ROUTE: Handle POST requests for delete file
161
- app.post('/:siteId/:pageUrl/:elementName/delete-file', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyDeleteFilePostHandler());
165
+ app.post('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFileDeletePostHandler());
162
166
 
163
167
  // 📥 -- ROUTE: Handle POST requests for review page. The `submit` action
164
168
  app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
@@ -10,6 +10,7 @@ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
10
10
  import { errorResponse } from "../utils/govcyApiResponse.mjs";
11
11
  import { isApiRequest } from '../utils/govcyApiDetection.mjs';
12
12
 
13
+ /* c8 ignore start */
13
14
  /**
14
15
  * Middleware to check if the user is authenticated. If not, redirect to the login page.
15
16
  *
@@ -136,4 +137,5 @@ export function handleLogout() {
136
137
  res.redirect(logoutUrl);
137
138
  });
138
139
  };
139
- }
140
+ }
141
+ /* c8 ignore end */
@@ -0,0 +1,320 @@
1
+ import * as govcyResources from "../resources/govcyResources.mjs";
2
+ import * as dataLayer from "../utils/govcyDataLayer.mjs";
3
+ import { logger } from "../utils/govcyLogger.mjs";
4
+ import { pageContainsFileInput } from "../utils/govcyHandleFiles.mjs";
5
+ import { whatsIsMyEnvironment, getEnvVariable, getEnvVariableBool } from '../utils/govcyEnvVariables.mjs';
6
+ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
7
+ import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
8
+ import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
9
+ import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
10
+ import { URL } from "url";
11
+
12
+
13
+ /**
14
+ * Middleware to handle the delete file page.
15
+ * This middleware processes the delete file page, populates the question, and shows validation errors.
16
+ */
17
+ export function govcyFileDeletePageHandler() {
18
+ return (req, res, next) => {
19
+ try {
20
+ const { siteId, pageUrl, elementName } = req.params;
21
+
22
+ // Create a deep copy of the service to avoid modifying the original
23
+ let serviceCopy = req.serviceData;
24
+
25
+ // ⤵️ Find the current page based on the URL
26
+ const page = getPageConfigData(serviceCopy, pageUrl);
27
+
28
+ // deep copy the page template to avoid modifying the original
29
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
30
+
31
+ // ----- Conditional logic comes here
32
+ // ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
33
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
34
+ if (conditionResult.result === false) {
35
+ logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
36
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
37
+ }
38
+
39
+ // Validate the field: Only allow delete if the page contains a fileInput with the given name
40
+ const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
41
+ if (!fileInputElement) {
42
+ return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
43
+ }
44
+
45
+ // Validate if the file input has a label
46
+ if (!fileInputElement?.params?.label) {
47
+ return handleMiddlewareError(`File input [${elementName}] does not have a label`, 404, next);
48
+ }
49
+
50
+ //get element data
51
+ const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
52
+
53
+ // If the element data is not found, return an error response
54
+ if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
55
+ return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
56
+ }
57
+
58
+ // Deep copy page title (so we don’t mutate template)
59
+ const pageTitle = JSON.parse(JSON.stringify(govcyResources.staticResources.text.deleteFileTitle));
60
+
61
+ // Replace label placeholders on page title
62
+ for (const lang of Object.keys(pageTitle)) {
63
+ const labelForLang = fileInputElement.params.label[lang] || fileInputElement.params.label.el || "";
64
+ pageTitle[lang] = pageTitle[lang].replace("{{file}}", labelForLang);
65
+ }
66
+
67
+
68
+ // Deep copy renderer pageData from
69
+ let pageData = JSON.parse(JSON.stringify(govcyResources.staticResources.rendererPageData));
70
+
71
+ // Handle isTesting
72
+ pageData.site.isTesting = (whatsIsMyEnvironment() === "staging");
73
+
74
+ // Base page template structure
75
+ let pageTemplate = {
76
+ sections: [
77
+ {
78
+ name: "beforeMain",
79
+ elements: [govcyResources.staticResources.elements.backLink]
80
+ }
81
+ ]
82
+ };
83
+
84
+ //contruct the warning if the file was uploaded more than once
85
+ const warningSameFile = {
86
+ element: "warning",
87
+ params: {
88
+ text: govcyResources.staticResources.text.deleteSameFileWarning,
89
+ }
90
+ };
91
+
92
+ const showSameFileWarning = dataLayer.isFileUsedInSiteInputDataAgain(
93
+ req.session,
94
+ siteId,
95
+ elementData
96
+ );
97
+
98
+ // Construct page title
99
+ const pageRadios = {
100
+ element: "radios",
101
+ params: {
102
+ id: "deleteFile",
103
+ name: "deleteFile",
104
+ legend: pageTitle,
105
+ isPageHeading: true,
106
+ classes: "govcy-mb-6",// only include the warning block when the file is referenced >1 times
107
+ elements: showSameFileWarning ? [warningSameFile] : [],
108
+ items: [
109
+ {
110
+ value: "yes",
111
+ text: govcyResources.staticResources.text.deleteYesOption
112
+ },
113
+ {
114
+ value: "no",
115
+ text: govcyResources.staticResources.text.deleteNoOption
116
+ }
117
+ ]
118
+
119
+ }
120
+ };
121
+ //-------------
122
+
123
+ // Construct submit button
124
+ const formElement = {
125
+ element: "form",
126
+ params: {
127
+ action: govcyResources.constructPageUrl(siteId, `${pageUrl}/delete-file/${elementName}`, (req.query.route === "review" ? "review" : "")),
128
+ method: "POST",
129
+ elements: [
130
+ pageRadios,
131
+ {
132
+ element: "button",
133
+ params: {
134
+ type: "submit",
135
+ text: govcyResources.staticResources.text.continue
136
+ }
137
+ },
138
+ govcyResources.csrfTokenInput(req.csrfToken())
139
+ ]
140
+ }
141
+ };
142
+
143
+ // --------- Handle Validation Errors ---------
144
+ // Check if validation errors exist in the request
145
+ const validationErrors = [];
146
+ let mainElements = [];
147
+ if (req?.query?.hasError) {
148
+ validationErrors.push({
149
+ link: '#deleteFile-option-1',
150
+ text: govcyResources.staticResources.text.deleteFileValidationError
151
+ });
152
+ mainElements.push(govcyResources.errorSummary(validationErrors));
153
+ formElement.params.elements[0].params.error = govcyResources.staticResources.text.deleteFileValidationError;
154
+ }
155
+ //--------- End Handle Validation Errors ---------
156
+
157
+ // Add elements to the main section, the H1, summary list, the submit button and the JS
158
+ mainElements.push(formElement);
159
+ // Append generated summary list to the page template
160
+ pageTemplate.sections.push({ name: "main", elements: mainElements });
161
+
162
+ //if user is logged in add he user bane section in the page template
163
+ if (dataLayer.getUser(req.session)) {
164
+ pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
165
+ }
166
+
167
+ //prepare pageData
168
+ pageData.site = serviceCopy.site;
169
+ pageData.pageData.title = pageTitle;
170
+
171
+ // Attach processed page data to the request
172
+ req.processedPage = {
173
+ pageData: pageData,
174
+ pageTemplate: pageTemplate
175
+ };
176
+ logger.debug("Processed delete file page data:", req.processedPage);
177
+ next();
178
+ } catch (error) {
179
+ return next(error); // Pass error to govcyHttpErrorHandler
180
+ }
181
+ };
182
+ }
183
+
184
+
185
+ /**
186
+ * Middleware to handle delete file post form processing
187
+ * This middleware processes the post, validates the form and handles the file data layer
188
+ */
189
+ export function govcyFileDeletePostHandler() {
190
+ return async (req, res, next) => {
191
+ try {
192
+ // Extract siteId and pageUrl from request
193
+ let { siteId, pageUrl, elementName } = req.params;
194
+
195
+ // get service data
196
+ let serviceCopy = req.serviceData;
197
+
198
+ // 🔍 Find the page by pageUrl
199
+ const page = getPageConfigData(serviceCopy, pageUrl);
200
+
201
+ // Deep copy pageTemplate to avoid modifying the original
202
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
203
+
204
+ // ----- Conditional logic comes here
205
+ // Check if the page has conditions and apply logic
206
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
207
+ if (conditionResult.result === false) {
208
+ logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
209
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
210
+ }
211
+
212
+ // Validate the field: Only allow delete if the page contains a fileInput with the given name
213
+ const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
214
+ if (!fileInputElement) {
215
+ return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
216
+ }
217
+
218
+ //get element data
219
+ const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
220
+
221
+ // If the element data is not found, return an error response
222
+ if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
223
+ return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
224
+ }
225
+
226
+ // the page base return url
227
+ const pageBaseReturnUrl = `http://localhost:3000/${siteId}/${pageUrl}`;
228
+
229
+ //check if input `deleteFile` has a value
230
+ if (!req?.body?.deleteFile ||
231
+ (req.body.deleteFile !== "yes" && req.body.deleteFile !== "no")) {
232
+ logger.debug("⛔️ No deleteFile value provided on POST — skipping form save and redirecting:", req.body);
233
+ //construct the page url with error
234
+ let myUrl = new URL(pageBaseReturnUrl + `/delete-file/${elementName}`);
235
+ //check if the route is review
236
+ if (req.query.route === "review") {
237
+ myUrl.searchParams.set("route", "review");
238
+ }
239
+ //set the error flag
240
+ myUrl.searchParams.set("hasError", "1");
241
+
242
+ //redirect to the same page with error summary (relative path)
243
+ return res.redirect(govcyResources.constructErrorSummaryUrl(myUrl.pathname + myUrl.search));
244
+ }
245
+
246
+ //if no validation errors
247
+ if (req.body.deleteFile === "yes") {
248
+ // Try to delete the file via the delete API.
249
+ // If it fails, log the error but continue to remove the file from the session
250
+ try {
251
+ // Get the delete file configuration
252
+ const deleteCfg = serviceCopy?.site?.fileDeleteAPIEndpoint;
253
+ // Check if download file configuration is available
254
+ if (!deleteCfg?.url || !deleteCfg?.clientKey || !deleteCfg?.serviceId) {
255
+ return handleMiddlewareError(`File delete APU configuration not found`, 404, next);
256
+ }
257
+
258
+ // Environment vars
259
+ const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
260
+ let url = getEnvVariable(deleteCfg.url || "", false);
261
+ const clientKey = getEnvVariable(deleteCfg.clientKey || "", false);
262
+ const serviceId = getEnvVariable(deleteCfg.serviceId || "", false);
263
+ const dsfGtwKey = getEnvVariable(deleteCfg?.dsfgtwApiKey || "", "");
264
+ const method = (deleteCfg?.method || "GET").toLowerCase();
265
+
266
+ // Check if the upload API is configured correctly
267
+ if (!url || !clientKey) {
268
+ return handleMiddlewareError(`Missing environment variables for upload`, 404, next);
269
+ }
270
+
271
+ // Construct the URL with tag being the elementName
272
+ url += `/${encodeURIComponent(elementData.fileId)}/${encodeURIComponent(elementData.sha256)}`;
273
+
274
+ // Get the user
275
+ const user = dataLayer.getUser(req.session);
276
+ // Perform the delete request
277
+ const response = await govcyApiRequest(
278
+ method,
279
+ url,
280
+ {},
281
+ true,
282
+ user,
283
+ {
284
+ accept: "text/plain",
285
+ "client-key": clientKey,
286
+ "service-id": serviceId,
287
+ ...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
288
+ },
289
+ 3,
290
+ allowSelfSignedCerts
291
+ );
292
+
293
+ // If not succeeded, handle error
294
+ if (!response?.Succeeded) {
295
+ logger.error("fileDeleteAPIEndpoint returned succeeded false");
296
+ }
297
+
298
+ } catch (error) {
299
+ logger.error(`fileDeleteAPIEndpoint Call failed: ${error.message}`);
300
+ }
301
+ // if succeeded all good
302
+ // dataLayer.storePageDataElement(req.session, siteId, pageUrl, elementName, "");
303
+ dataLayer.removeAllFilesFromSite(req.session, siteId, { fileId: elementData.fileId, sha256: elementData.sha256 });
304
+ logger.info(`File deleted by user`, { siteId, pageUrl, elementName });
305
+ }
306
+ // construct the page url
307
+ let myUrl = new URL(pageBaseReturnUrl);
308
+ //check if the route is review
309
+ if (req.query.route === "review") {
310
+ myUrl.searchParams.set("route", "review");
311
+ }
312
+
313
+ // redirect to the page (relative path)
314
+ res.redirect(myUrl.pathname + myUrl.search);
315
+ } catch (error) {
316
+ logger.error("Error in govcyFileDeletePostHandler middleware:", error.message);
317
+ return next(error); // Pass error to govcyHttpErrorHandler
318
+ }
319
+ };
320
+ }
@@ -13,7 +13,7 @@ const upload = multer({
13
13
 
14
14
 
15
15
 
16
- export const govcyUploadMiddleware = [
16
+ export const govcyFileUpload = [
17
17
  upload.single('file'), // multer parses the uploaded file and stores it in req.file
18
18
 
19
19
  async function govcyUploadHandler(req, res) {
@@ -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
  * Middleware function to render PDFs using the GovCy Frontend Renderer.
7
7
  * This function takes the processed page data and template, and generates the final PDF response.
8
8
  */
9
+ /* c8 ignore start */
9
10
  export function govcyPDFRender() {
10
11
  return async (req, res) => {
11
12
  try {
@@ -29,4 +30,5 @@ export function govcyPDFRender() {
29
30
  res.status(500).send('Unable to generate PDF at this time.');
30
31
  }
31
32
  };
32
- }
33
+ }
34
+ /* c8 ignore end */
@@ -53,11 +53,7 @@ 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
59
- element.params.elements.push(govcyResources.staticResources.elements["govcyFormsJs"]);
60
-
56
+
61
57
  // 🔍 Find the first button with `prototypeNavigate`
62
58
  const button = element.params.elements.find(subElement =>
63
59
  // subElement.element === "button" && subElement.params.prototypeNavigate
@@ -1,4 +1,5 @@
1
1
  import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
2
+ import * as govcyResources from "../resources/govcyResources.mjs";
2
3
 
3
4
  /**
4
5
  * Middleware function to render pages using the GovCy Frontend Renderer.
@@ -6,8 +7,17 @@ import { govcyFrontendRenderer } from "@gov-cy/govcy-frontend-renderer";
6
7
  */
7
8
  export function renderGovcyPage() {
8
9
  return (req, res) => {
10
+ const afterBody = {
11
+ name: "afterBody",
12
+ elements: [
13
+ govcyResources.staticResources.elements["govcyLoadingOverlay"],
14
+ govcyResources.staticResources.elements["govcyFormsJs"]
15
+ ]
16
+ };
17
+ // Initialize the renderer
9
18
  const renderer = new govcyFrontendRenderer();
10
19
  const { processedPage } = req;
20
+ processedPage.pageTemplate.sections.push(afterBody);
11
21
  const html = renderer.renderFromJSON(processedPage.pageTemplate, processedPage.pageData);
12
22
  res.send(html);
13
23
  };