@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/README.md +1033 -79
- package/package.json +9 -3
- package/src/auth/cyLoginAuth.mjs +2 -1
- package/src/index.mjs +9 -5
- package/src/middleware/cyLoginAuth.mjs +3 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +320 -0
- package/src/middleware/{govcyUpload.mjs → govcyFileUpload.mjs} +1 -1
- package/src/middleware/govcyFileViewHandler.mjs +161 -0
- package/src/middleware/govcyPDFRender.mjs +3 -1
- package/src/middleware/govcyPageHandler.mjs +1 -5
- package/src/middleware/govcyPageRender.mjs +10 -0
- package/src/middleware/govcyReviewPageHandler.mjs +4 -1
- package/src/middleware/govcyReviewPostHandler.mjs +1 -1
- package/src/middleware/govcySuccessPageHandler.mjs +2 -3
- package/src/public/js/govcyFiles.js +108 -5
- package/src/resources/govcyResources.mjs +26 -6
- package/src/utils/govcyConstants.mjs +1 -1
- package/src/utils/govcyDataLayer.mjs +192 -14
- package/src/utils/govcyFormHandling.mjs +3 -2
- package/src/utils/govcyHandleFiles.mjs +3 -3
- package/src/utils/govcySubmitData.mjs +162 -109
- package/src/middleware/govcyDeleteFileHandler.mjs +0 -234
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gov-cy/govcy-express-services",
|
|
3
|
-
"version": "1.
|
|
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.
|
|
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",
|
package/src/auth/cyLoginAuth.mjs
CHANGED
|
@@ -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 {
|
|
28
|
-
import {
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
-
|
|
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
|
};
|