@gov-cy/govcy-express-services 1.0.0-alpha.1 → 1.0.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -494,7 +494,9 @@ Content-Type: application/json
494
494
  The API is expected to return a JSON response with the following structure (see [govcyApiRequest.mjs](src/utils/govcyApiRequest.mjs) for normalization):
495
495
 
496
496
  **On Success:**
497
- ```json
497
+ ```http
498
+ HTTP/1.1 200 OK
499
+
498
500
  {
499
501
  "Succeeded": true,
500
502
  "ErrorCode": 0,
@@ -503,7 +505,9 @@ The API is expected to return a JSON response with the following structure (see
503
505
  ```
504
506
 
505
507
  **On Failure:**
506
- ```json
508
+ ```http
509
+ HTTP/1.1 200 OK
510
+
507
511
  {
508
512
  "Succeeded": false,
509
513
  "ErrorCode": 102,
@@ -637,7 +641,9 @@ Content-Type: application/json
637
641
  The API is expected to return a JSON response with the following structure (see [govcyApiRequest.mjs](src/utils/govcyApiRequest.mjs) for normalization):
638
642
 
639
643
  **On Success:**
640
- ```json
644
+ ```http
645
+ HTTP/1.1 200 OK
646
+
641
647
  {
642
648
  "Succeeded": true,
643
649
  "ErrorCode": 0,
@@ -649,7 +655,9 @@ The API is expected to return a JSON response with the following structure (see
649
655
  ```
650
656
 
651
657
  **On Failure:**
652
- ```json
658
+ ```http
659
+ HTTP/1.1 200 OK
660
+
653
661
  {
654
662
  "Succeeded": false,
655
663
  "ErrorCode": 102,
@@ -1419,8 +1427,8 @@ Explanation:
1419
1427
  The **temporary save** feature allows user progress to be stored in an external API and automatically reloaded on the next visit.
1420
1428
  This is useful for long forms or cases where users may leave and return later.
1421
1429
 
1422
- #### 1. Configure the endpoints in your service JSON
1423
- In your service’s `site` object, add both a `submissionGetAPIEndpoint` and `submissionPutAPIEndpoint` entry:
1430
+ #### How to configure temporary save
1431
+ To use this feature, configure the config JSON file. In your service’s `site` object, add both a `submissionGetAPIEndpoint` and `submissionPutAPIEndpoint` entry:
1424
1432
 
1425
1433
  ```json
1426
1434
  "submissionGetAPIEndpoint": {
@@ -1439,7 +1447,6 @@ In your service’s `site` object, add both a `submissionGetAPIEndpoint` and `su
1439
1447
 
1440
1448
  These values should point to environment variables that hold your real endpoint URLs and credentials.
1441
1449
 
1442
- #### 2. Add environment variables
1443
1450
  In your `secrets/.env` file (and staging/production configs), define the variables referenced above:
1444
1451
 
1445
1452
  ```dotenv
@@ -1449,17 +1456,155 @@ TEST_SUBMISSION_API_CLIENT_KEY=12345678901234567890123456789000
1449
1456
  TEST_SUBMISSION_API_SERVICE_ID=123
1450
1457
  ```
1451
1458
 
1452
- #### 3. How it works
1459
+ #### How temporary save works
1453
1460
 
1454
- - **On first page load** for a site, `govcyLoadSubmissionData` will:
1461
+ - **On first page load** for a site, using the `submissionGetAPIEndpoint` the system will:
1455
1462
  1. Call the GET endpoint to retrieve any saved submission.
1456
1463
  2. If found, populate the session’s `inputData` so fields are pre-filled.
1457
1464
  3. If not found, call the PUT endpoint to create a new temporary record.
1458
1465
  - **On every form POST**, after successful validation:
1459
- - The `govcyFormsPostHandler` will fire-and-forget a `PUT` request to update the saved submission with the latest form data.
1466
+ - The `submissionPutAPIEndpoint` will fire-and-forget a `PUT` request to update the saved submission with the latest form data.
1460
1467
  - The payload includes all required submission fields with `submission_data` JSON-stringified.
1461
1468
 
1462
- #### 4. Backward compatibility
1469
+ #### `submissionGetAPIEndpoint` `GET` API Request and Response
1470
+ This API is used to retrieve the saved submission data.
1471
+
1472
+ **Request:**
1473
+
1474
+ - **HTTP Method**: GET
1475
+ - **URL**: Resolved from the url property in your config (from the environment variable).
1476
+ - **Headers**:
1477
+ - **Authorization**: `Bearer <access_token>` (form user's cyLogin access token)
1478
+ - **client-key**: `<clientKey>` (from config/env)
1479
+ - **service-id**: `<serviceId>` (from config/env)
1480
+ - **Accept**: `text/plain`
1481
+ - **Body**: The body contains the and either:
1482
+ - an a `null` which means no data was found for the user
1483
+ - a JSON object with all the form data collected from the user across all pages in previous sessions.
1484
+
1485
+ **Example Request:**
1486
+
1487
+ ```http
1488
+ GET /temp-save-get-endpoint?status=0 HTTP/1.1
1489
+ Host: localhost:3002
1490
+ Authorization: Bearer eyJhbGciOi...
1491
+ client-key: 12345678901234567890123456789000
1492
+ service-id: 123
1493
+ Accept: text/plain
1494
+ Content-Type: application/json
1495
+ ```
1496
+
1497
+ **Response:**
1498
+
1499
+ The API is expected to return a JSON response with the following structure:
1500
+
1501
+ **When temporary submission data are found:**
1502
+
1503
+ ```http
1504
+ HTTP/1.1 200 OK
1505
+
1506
+ {
1507
+ "Succeeded": true,
1508
+ "ErrorCode": 0,
1509
+ "ErrorMessage": null,
1510
+ "Data": {
1511
+ "submissionData": "{\"index\":{\"formData\":{\"certificate_select\":[\"birth\",\"permanent_residence\"]}},\"data-entry-radios\":{\"formData\":{\"mobile_select\":\"other\",\"mobileTxt\":\"+35799484967\"}}}"
1512
+ }
1513
+ }
1514
+ ```
1515
+
1516
+ **When temporary submission data are NOT found:**
1517
+
1518
+ ```http
1519
+ HTTP/1.1 404 Not Found
1520
+
1521
+ {
1522
+ "Succeeded": true,
1523
+ "ErrorCode": 0,
1524
+ "ErrorMessage": null,
1525
+ "Data": null
1526
+ }
1527
+ ```
1528
+
1529
+ **When temporary submission retreival fails:**
1530
+
1531
+ ```http
1532
+ HTTP/1.1 200 OK
1533
+
1534
+ {
1535
+ "Succeeded": false,
1536
+ "ErrorCode": 401,
1537
+ "ErrorMessage": "Not authorized",
1538
+ "Data": null
1539
+ }
1540
+ ```
1541
+
1542
+ #### `submissionPutAPIEndpoint` `PUT` API Request and Response
1543
+ This API is used to temporary save the submission data.
1544
+
1545
+ **Request:**
1546
+
1547
+ - **HTTP Method**: PUT
1548
+ - **URL**: Resolved from the url property in your config (from the environment variable).
1549
+ - **Headers**:
1550
+ - **Authorization**: `Bearer <access_token>` (form user's cyLogin access token)
1551
+ - **client-key**: `<clientKey>` (from config/env)
1552
+ - **service-id**: `<serviceId>` (from config/env)
1553
+ - **Accept**: `text/plain`
1554
+ - **Body**: The body contains the a JSON object with all the form data collected from the user across all pages.
1555
+
1556
+ **Example Request:**
1557
+
1558
+ ```http
1559
+ PUT /temp-save-endpoint HTTP/1.1
1560
+ Host: localhost:3002
1561
+ Authorization: Bearer eyJhbGciOi...
1562
+ client-key: 12345678901234567890123456789000
1563
+ service-id: 123
1564
+ Accept: text/plain
1565
+ Content-Type: application/json
1566
+
1567
+ {
1568
+ "submission_data" : "{\"index\":{\"formData\":{\"certificate_select\":[\"birth\",\"permanent_residence\"]}},\"data-entry-radios\":{\"formData\":{\"mobile_select\":\"other\",\"mobileTxt\":\"+35799484967\"}}}"
1569
+ }
1570
+ ```
1571
+
1572
+ **Response:**
1573
+
1574
+ The API is expected to return a JSON response with the following structure:
1575
+
1576
+ **On success:**
1577
+
1578
+ ```http
1579
+ HTTP/1.1 200 OK
1580
+
1581
+ {
1582
+ "Succeeded": true,
1583
+ "ErrorCode": 0,
1584
+ "ErrorMessage": null,
1585
+ "Data": {
1586
+ "submissionData": "{\"index\":{\"formData\":{\"certificate_select\":[\"birth\",\"permanent_residence\"]}},\"data-entry-radios\":{\"formData\":{\"mobile_select\":\"other\",\"mobileTxt\":\"+35799484967\"}}}"
1587
+ }
1588
+ }
1589
+ ```
1590
+
1591
+ **On failure:**
1592
+
1593
+ ```http
1594
+ HTTP/1.1 401 Unauthorized
1595
+
1596
+ {
1597
+ "Succeeded": false,
1598
+ "ErrorCode": 401,
1599
+ "ErrorMessage": "Not authorized",
1600
+ "Data": null
1601
+ }
1602
+ ```
1603
+
1604
+ **Notes**:
1605
+ - The response is normalized to always use PascalCase keys (`Succeeded`, `ErrorCode`, etc.), regardless of the backend’s casing.
1606
+
1607
+ #### Temporary save backward compatibility
1463
1608
  If these endpoints are not defined in the service JSON, the temporary save/load logic is skipped entirely.
1464
1609
  Existing services will continue to work without modification.
1465
1610
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.0.0-alpha.1",
3
+ "version": "1.0.0-alpha.10",
4
4
  "description": "An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.",
5
5
  "author": "DMRID - DSF Team",
6
6
  "license": "MIT",
@@ -48,12 +48,14 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "@gov-cy/dsf-email-templates": "^2.1.0",
51
- "@gov-cy/govcy-frontend-renderer": "^1.18.0",
51
+ "@gov-cy/govcy-frontend-renderer": "^1.21.0",
52
52
  "axios": "^1.9.0",
53
53
  "cookie-parser": "^1.4.7",
54
54
  "dotenv": "^16.3.1",
55
55
  "express": "^4.18.2",
56
56
  "express-session": "^1.17.3",
57
+ "form-data": "^4.0.4",
58
+ "multer": "^2.0.2",
57
59
  "openid-client": "^6.3.4",
58
60
  "puppeteer": "^24.6.0"
59
61
  },
package/src/index.mjs CHANGED
@@ -24,6 +24,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 { govcyFileUpload } from './middleware/govcyFileUpload.mjs';
28
+ import { govcyFileDeletePageHandler, govcyFileDeletePostHandler } from './middleware/govcyFileDeleteHandler.mjs';
29
+ import { govcyFileViewHandler } from './middleware/govcyFileViewHandler.mjs';
27
30
  import { isProdOrStaging , getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
28
31
  import { logger } from "./utils/govcyLogger.mjs";
29
32
 
@@ -128,6 +131,14 @@ export default function initializeGovCyExpressService(){
128
131
 
129
132
  // 📝 -- ROUTE: Serve manifest.json dynamically for each site
130
133
  app.get('/:siteId/manifest.json', serviceConfigDataMiddleware, govcyManifestHandler());
134
+
135
+ // 🗃️ -- ROUTE: Handle POST requests for file uploads for a page.
136
+ app.post('/apis/:siteId/:pageUrl/upload',
137
+ serviceConfigDataMiddleware,
138
+ requireAuth, // UNCOMMENT
139
+ naturalPersonPolicy, // UNCOMMENT
140
+ govcyServiceEligibilityHandler(true), // UNCOMMENT
141
+ govcyFileUpload);
131
142
 
132
143
  // 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
133
144
  app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyLoadSubmissionData(),govcyPageHandler(), renderGovcyPage());
@@ -141,16 +152,24 @@ export default function initializeGovCyExpressService(){
141
152
  // ✅ -- ROUTE: Add Success Page Route (BEFORE the dynamic route)
142
153
  app.get('/:siteId/success',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
143
154
 
155
+ // 👀🗃️ -- ROUTE: View file (BEFORE the dynamic route)
156
+ app.get('/:siteId/:pageUrl/view-file/:elementName', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileViewHandler());
157
+
158
+ // ❌🗃️ -- ROUTE: Delete file (BEFORE the dynamic route)
159
+ app.get('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileDeletePageHandler(), renderGovcyPage());
160
+
144
161
  // 📝 -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
145
162
  app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
146
163
 
164
+ // ❌🗃️📥 -- ROUTE: Handle POST requests for delete file
165
+ app.post('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFileDeletePostHandler());
166
+
147
167
  // 📥 -- ROUTE: Handle POST requests for review page. The `submit` action
148
168
  app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
149
169
 
150
170
  // 👀📥 -- ROUTE: Handle POST requests (Form Submissions) based on siteId and pageUrl, using govcyFormsPostHandler middleware
151
171
  app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
152
172
 
153
-
154
173
  // post for /:siteId/review
155
174
 
156
175
  // 🔹 Catch 404 errors (must be after all routes)
@@ -7,6 +7,8 @@
7
7
  import { getLoginUrl, handleCallback, getLogoutUrl } from '../auth/cyLoginAuth.mjs';
8
8
  import { logger } from "../utils/govcyLogger.mjs";
9
9
  import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
10
+ import { errorResponse } from "../utils/govcyApiResponse.mjs";
11
+ import { isApiRequest } from '../utils/govcyApiDetection.mjs';
10
12
 
11
13
  /**
12
14
  * Middleware to check if the user is authenticated. If not, redirect to the login page.
@@ -17,6 +19,12 @@ import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
17
19
  */
18
20
  export function requireAuth(req, res, next) {
19
21
  if (!req.session.user) {
22
+ if (isApiRequest(req)) {
23
+ const err = new Error("Unauthorized: user not authenticated");
24
+ err.status = 401;
25
+ return next(err);
26
+ }
27
+
20
28
  // Store the original URL before redirecting to login
21
29
  req.session.redirectAfterLogin = req.originalUrl;
22
30
  return res.redirect('/login');
@@ -1,5 +1,8 @@
1
1
 
2
2
  import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
3
+ import { errorResponse } from '../utils/govcyApiResponse.mjs';
4
+ import { isApiRequest } from '../utils/govcyApiDetection.mjs';
5
+
3
6
  /**
4
7
  * Middleware to handle CSRF token generation and validation.
5
8
  *
@@ -14,7 +17,18 @@ export function govcyCsrfMiddleware(req, res, next) {
14
17
  }
15
18
 
16
19
  req.csrfToken = () => req.session.csrfToken;
17
-
20
+
21
+ if (
22
+ req.method === 'POST' &&
23
+ req.headers['content-type']?.includes('multipart/form-data') &&
24
+ isApiRequest(req)) {
25
+ const tokenFromHeader = req.get('X-CSRF-Token');
26
+ // UNCOMMENT
27
+ if (!tokenFromHeader || tokenFromHeader !== req.session.csrfToken) {
28
+ return res.status(400).json(errorResponse(403, 'Invalid CSRF token'));
29
+ }
30
+ return next();
31
+ }
18
32
  // Check token on POST requests
19
33
  if (req.method === 'POST') {
20
34
  const tokenFromBody = req.body._csrf;
@@ -0,0 +1,238 @@
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 } 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 { URL } from "url";
10
+
11
+
12
+ /**
13
+ * Middleware to handle the delete file page.
14
+ * This middleware processes the delete file page, populates the question, and shows validation errors.
15
+ */
16
+ export function govcyFileDeletePageHandler() {
17
+ return (req, res, next) => {
18
+ try {
19
+ const { siteId, pageUrl, elementName } = req.params;
20
+
21
+ // Create a deep copy of the service to avoid modifying the original
22
+ let serviceCopy = req.serviceData;
23
+
24
+ // ⤵️ Find the current page based on the URL
25
+ const page = getPageConfigData(serviceCopy, pageUrl);
26
+
27
+ // deep copy the page template to avoid modifying the original
28
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
29
+
30
+ // ----- Conditional logic comes here
31
+ // ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
32
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
33
+ if (conditionResult.result === false) {
34
+ logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
35
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
36
+ }
37
+
38
+ // Validate the field: Only allow delete if the page contains a fileInput with the given name
39
+ const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
40
+ if (!fileInputElement) {
41
+ return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
42
+ }
43
+
44
+ // Validate if the file input has a label
45
+ if (!fileInputElement?.params?.label) {
46
+ return handleMiddlewareError(`File input [${elementName}] does not have a label`, 404, next);
47
+ }
48
+
49
+ //get element data
50
+ const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName)
51
+
52
+ // If the element data is not found, return an error response
53
+ if (!elementData) {
54
+ return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
55
+ }
56
+
57
+ // Deep copy page title (so we don’t mutate template)
58
+ const pageTitle = JSON.parse(JSON.stringify(govcyResources.staticResources.text.deleteFileTitle));
59
+
60
+ // Replace label placeholders on page title
61
+ for (const lang of Object.keys(pageTitle)) {
62
+ const labelForLang = fileInputElement.params.label[lang] || fileInputElement.params.label.el || "";
63
+ pageTitle[lang] = pageTitle[lang].replace("{{file}}", labelForLang);
64
+ }
65
+
66
+
67
+ // Deep copy renderer pageData from
68
+ let pageData = JSON.parse(JSON.stringify(govcyResources.staticResources.rendererPageData));
69
+
70
+ // Handle isTesting
71
+ pageData.site.isTesting = (whatsIsMyEnvironment() === "staging");
72
+
73
+ // Base page template structure
74
+ let pageTemplate = {
75
+ sections: [
76
+ {
77
+ name: "beforeMain",
78
+ elements: [govcyResources.staticResources.elements.backLink]
79
+ }
80
+ ]
81
+ };
82
+
83
+ // Construct page title
84
+ const pageRadios = {
85
+ element: "radios",
86
+ params: {
87
+ id: "deleteFile",
88
+ name: "deleteFile",
89
+ legend: pageTitle,
90
+ isPageHeading: true,
91
+ classes: "govcy-mb-6",
92
+ items: [
93
+ {
94
+ value: "yes",
95
+ text: govcyResources.staticResources.text.deleteYesOption
96
+ },
97
+ {
98
+ value: "no",
99
+ text: govcyResources.staticResources.text.deleteNoOption
100
+ }
101
+ ]
102
+
103
+ }
104
+ };
105
+ //-------------
106
+
107
+ // Construct submit button
108
+ const formElement = {
109
+ element: "form",
110
+ params: {
111
+ action: govcyResources.constructPageUrl(siteId, `${pageUrl}/delete-file/${elementName}`, (req.query.route === "review" ? "review" : "")),
112
+ method: "POST",
113
+ elements: [
114
+ pageRadios,
115
+ {
116
+ element: "button",
117
+ params: {
118
+ type: "submit",
119
+ text: govcyResources.staticResources.text.continue
120
+ }
121
+ },
122
+ govcyResources.csrfTokenInput(req.csrfToken())
123
+ ]
124
+ }
125
+ };
126
+
127
+ // --------- Handle Validation Errors ---------
128
+ // Check if validation errors exist in the request
129
+ const validationErrors = [];
130
+ let mainElements = [];
131
+ if (req?.query?.hasError) {
132
+ validationErrors.push({
133
+ link: '#deleteFile-option-1',
134
+ text: govcyResources.staticResources.text.deleteFileValidationError
135
+ });
136
+ mainElements.push(govcyResources.errorSummary(validationErrors));
137
+ formElement.params.elements[0].params.error = govcyResources.staticResources.text.deleteFileValidationError;
138
+ }
139
+ //--------- End Handle Validation Errors ---------
140
+
141
+ // Add elements to the main section, the H1, summary list, the submit button and the JS
142
+ mainElements.push(formElement, govcyResources.staticResources.elements["govcyFormsJs"]);
143
+ // Append generated summary list to the page template
144
+ pageTemplate.sections.push({ name: "main", elements: mainElements });
145
+
146
+ //if user is logged in add he user bane section in the page template
147
+ if (dataLayer.getUser(req.session)) {
148
+ pageTemplate.sections.push(govcyResources.userNameSection(dataLayer.getUser(req.session).name)); // Add user name section
149
+ }
150
+
151
+ //prepare pageData
152
+ pageData.site = serviceCopy.site;
153
+ pageData.pageData.title = pageTitle;
154
+
155
+ // Attach processed page data to the request
156
+ req.processedPage = {
157
+ pageData: pageData,
158
+ pageTemplate: pageTemplate
159
+ };
160
+ logger.debug("Processed delete file page data:", req.processedPage);
161
+ next();
162
+ } catch (error) {
163
+ return next(error); // Pass error to govcyHttpErrorHandler
164
+ }
165
+ };
166
+ }
167
+
168
+
169
+ /**
170
+ * Middleware to handle delete file post form processing
171
+ * This middleware processes the post, validates the form and handles the file data layer
172
+ */
173
+ export function govcyFileDeletePostHandler() {
174
+ return (req, res, next) => {
175
+ try {
176
+ // Extract siteId and pageUrl from request
177
+ let { siteId, pageUrl, elementName } = req.params;
178
+
179
+ // get service data
180
+ let serviceCopy = req.serviceData;
181
+
182
+ // 🔍 Find the page by pageUrl
183
+ const page = getPageConfigData(serviceCopy, pageUrl);
184
+
185
+ // Deep copy pageTemplate to avoid modifying the original
186
+ const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate));
187
+
188
+ // ----- Conditional logic comes here
189
+ // Check if the page has conditions and apply logic
190
+ const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
191
+ if (conditionResult.result === false) {
192
+ logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
193
+ return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
194
+ }
195
+
196
+ // Validate the field: Only allow delete if the page contains a fileInput with the given name
197
+ const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName);
198
+ if (!fileInputElement) {
199
+ return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next);
200
+ }
201
+ // the page base return url
202
+ const pageBaseReturnUrl = `http://localhost:3000/${siteId}/${pageUrl}`;
203
+
204
+ //check if input `deleteFile` has a value
205
+ if (!req?.body?.deleteFile ||
206
+ (req.body.deleteFile !== "yes" && req.body.deleteFile !== "no")) {
207
+ logger.debug("⛔️ No deleteFile value provided on POST — skipping form save and redirecting:", req.body);
208
+ //construct the page url with error
209
+ let myUrl = new URL(pageBaseReturnUrl + `/delete-file/${elementName}`);
210
+ //check if the route is review
211
+ if (req.query.route === "review") {
212
+ myUrl.searchParams.set("route", "review");
213
+ }
214
+ //set the error flag
215
+ myUrl.searchParams.set("hasError", "1");
216
+
217
+ //redirect to the same page with error summary (relative path)
218
+ return res.redirect(govcyResources.constructErrorSummaryUrl(myUrl.pathname + myUrl.search));
219
+ }
220
+
221
+ //if no validation errors
222
+ if (req.body.deleteFile === "yes") {
223
+ dataLayer.storePageDataElement(req.session, siteId, pageUrl, elementName, "");
224
+ }
225
+ // construct the page url
226
+ let myUrl = new URL(pageBaseReturnUrl);
227
+ //check if the route is review
228
+ if (req.query.route === "review") {
229
+ myUrl.searchParams.set("route", "review");
230
+ }
231
+
232
+ // redirect to the page (relative path)
233
+ res.redirect(myUrl.pathname + myUrl.search);
234
+ } catch (error) {
235
+ return next(error); // Pass error to govcyHttpErrorHandler
236
+ }
237
+ };
238
+ }
@@ -0,0 +1,36 @@
1
+ import multer from 'multer';
2
+ import { logger } from '../utils/govcyLogger.mjs';
3
+ import { successResponse, errorResponse } from '../utils/govcyApiResponse.mjs';
4
+ import { ALLOWED_MULTER_FILE_SIZE_MB } from "../utils/govcyConstants.mjs";
5
+ import { handleFileUpload } from "../utils/govcyHandleFiles.mjs";
6
+
7
+ // Configure multer to store the file in memory (not disk) and limit the size to 10MB
8
+ const upload = multer({
9
+ storage: multer.memoryStorage(),
10
+ limits: { fileSize: ALLOWED_MULTER_FILE_SIZE_MB * 1024 * 1024 } // 10MB
11
+ });
12
+
13
+
14
+
15
+
16
+ export const govcyFileUpload = [
17
+ upload.single('file'), // multer parses the uploaded file and stores it in req.file
18
+
19
+ async function govcyUploadHandler(req, res) {
20
+ const result = await handleFileUpload({
21
+ service: req.serviceData,
22
+ store: req.session,
23
+ siteId: req.params.siteId,
24
+ pageUrl: req.params.pageUrl,
25
+ elementName: req.body?.elementName,
26
+ file: req.file
27
+ });
28
+
29
+ if (result.status !== 200) {
30
+ logger.error("Upload failed", result);
31
+ return res.status(result.status).json(errorResponse(result.dataStatus, result.errorMessage || 'File upload failed'));
32
+ }
33
+
34
+ return res.json(successResponse(result.data));
35
+ }
36
+ ];