@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 CHANGED
@@ -32,6 +32,7 @@ The project is designed to support the [Linear structure](https://gov-cy.github.
32
32
  - [πŸ“€ Site submissions](#-site-submissions)
33
33
  - [βœ… Input validations](#-input-validations)
34
34
  - [βœ… Conditional logic](#-conditional-logic)
35
+ - [πŸ’Ύ Temporary save](#-temporary-save)
35
36
  - [πŸ›£οΈ Routes](#%EF%B8%8F-routes)
36
37
  - [πŸ‘¨β€πŸ’» Enviromental variables](#-enviromental-variables)
37
38
  - [πŸ”’ Security note](#-security-note)
@@ -55,6 +56,7 @@ The project is designed to support the [Linear structure](https://gov-cy.github.
55
56
  - Pre-filling posted values (in the same session)
56
57
  - Site level API eligibility checks
57
58
  - API integration with retry logic for form submissions.
59
+ - Optional temporary save of in-progress form data via configurable API endpoints
58
60
 
59
61
  ## πŸ“‹ Prerequisites
60
62
  - Node.js 20+
@@ -492,7 +494,9 @@ Content-Type: application/json
492
494
  The API is expected to return a JSON response with the following structure (see [govcyApiRequest.mjs](src/utils/govcyApiRequest.mjs) for normalization):
493
495
 
494
496
  **On Success:**
495
- ```json
497
+ ```http
498
+ HTTP/1.1 200 OK
499
+
496
500
  {
497
501
  "Succeeded": true,
498
502
  "ErrorCode": 0,
@@ -501,7 +505,9 @@ The API is expected to return a JSON response with the following structure (see
501
505
  ```
502
506
 
503
507
  **On Failure:**
504
- ```json
508
+ ```http
509
+ HTTP/1.1 200 OK
510
+
505
511
  {
506
512
  "Succeeded": false,
507
513
  "ErrorCode": 102,
@@ -635,7 +641,9 @@ Content-Type: application/json
635
641
  The API is expected to return a JSON response with the following structure (see [govcyApiRequest.mjs](src/utils/govcyApiRequest.mjs) for normalization):
636
642
 
637
643
  **On Success:**
638
- ```json
644
+ ```http
645
+ HTTP/1.1 200 OK
646
+
639
647
  {
640
648
  "Succeeded": true,
641
649
  "ErrorCode": 0,
@@ -647,7 +655,9 @@ The API is expected to return a JSON response with the following structure (see
647
655
  ```
648
656
 
649
657
  **On Failure:**
650
- ```json
658
+ ```http
659
+ HTTP/1.1 200 OK
660
+
651
661
  {
652
662
  "Succeeded": false,
653
663
  "ErrorCode": 102,
@@ -1412,6 +1422,192 @@ Explanation:
1412
1422
  - `[].concat(...)`: safely flattens a string or array into an array.
1413
1423
  - `.includes('value1')`: checks if the value is selected.
1414
1424
 
1425
+ ### πŸ’Ύ Temporary save
1426
+
1427
+ The **temporary save** feature allows user progress to be stored in an external API and automatically reloaded on the next visit.
1428
+ This is useful for long forms or cases where users may leave and return later.
1429
+
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:
1432
+
1433
+ ```json
1434
+ "submissionGetAPIEndpoint": {
1435
+ "url": "TEST_SUBMISSION_GET_API_URL",
1436
+ "method": "GET",
1437
+ "clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
1438
+ "serviceId": "TEST_SUBMISSION_API_SERVICE_ID"
1439
+ },
1440
+ "submissionPutAPIEndpoint": {
1441
+ "url": "TEST_SUBMISSION_PUT_API_URL",
1442
+ "method": "PUT",
1443
+ "clientKey": "TEST_SUBMISSION_API_CLIENT_KEY",
1444
+ "serviceId": "TEST_SUBMISSION_API_SERVICE_ID"
1445
+ }
1446
+ ```
1447
+
1448
+ These values should point to environment variables that hold your real endpoint URLs and credentials.
1449
+
1450
+ In your `secrets/.env` file (and staging/production configs), define the variables referenced above:
1451
+
1452
+ ```dotenv
1453
+ TEST_SUBMISSION_GET_API_URL=https://example.com/api/submissionData
1454
+ TEST_SUBMISSION_PUT_API_URL=https://example.com/api/submissionData
1455
+ TEST_SUBMISSION_API_CLIENT_KEY=12345678901234567890123456789000
1456
+ TEST_SUBMISSION_API_SERVICE_ID=123
1457
+ ```
1458
+
1459
+ #### How temporary save works
1460
+
1461
+ - **On first page load** for a site, using the `submissionGetAPIEndpoint` the system will:
1462
+ 1. Call the GET endpoint to retrieve any saved submission.
1463
+ 2. If found, populate the session’s `inputData` so fields are pre-filled.
1464
+ 3. If not found, call the PUT endpoint to create a new temporary record.
1465
+ - **On every form POST**, after successful validation:
1466
+ - The `submissionPutAPIEndpoint` will fire-and-forget a `PUT` request to update the saved submission with the latest form data.
1467
+ - The payload includes all required submission fields with `submission_data` JSON-stringified.
1468
+
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
1608
+ If these endpoints are not defined in the service JSON, the temporary save/load logic is skipped entirely.
1609
+ Existing services will continue to work without modification.
1610
+
1415
1611
  ### πŸ›£οΈ Routes
1416
1612
  The project uses express.js to serve the following routes:
1417
1613
 
@@ -1500,6 +1696,11 @@ TEST_SUBMISSION_API_CLIENT_KEY=12345678901234567890123456789000
1500
1696
  TEST_SUBMISSION_API_SERVIVE_ID=123
1501
1697
  TEST_SUBMISSION_DSF_GTW_KEY=12345678901234567890123456789000
1502
1698
 
1699
+ # Optional Temporary Save GET and PUT endpoint (test service)
1700
+ TEST_SUBMISSION_GET_API_URL=http://localhost:3002/getTempSubmission
1701
+ TEST_SUBMISSION_PUT_API_URL=http://localhost:3002/save
1702
+
1703
+
1503
1704
  # Eligibility checks (optional test APIs)
1504
1705
  TEST_ELIGIBILITY_1_API_URL=http://localhost:3002/eligibility1
1505
1706
  TEST_ELIGIBILITY_2_API_URL=http://localhost:3002/eligibility2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "0.2.16",
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
@@ -23,6 +23,10 @@ import { serviceConfigDataMiddleware } from './middleware/govcyConfigSiteData.mj
23
23
  import { govcyManifestHandler } from './middleware/govcyManifestHandler.mjs';
24
24
  import { govcyRoutePageHandler } from './middleware/govcyRoutePageHandler.mjs';
25
25
  import { govcyServiceEligibilityHandler } from './middleware/govcyServiceEligibilityHandler.mjs';
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';
26
30
  import { isProdOrStaging , getEnvVariable, whatsIsMyEnvironment } from './utils/govcyEnvVariables.mjs';
27
31
  import { logger } from "./utils/govcyLogger.mjs";
28
32
 
@@ -127,12 +131,20 @@ export default function initializeGovCyExpressService(){
127
131
 
128
132
  // πŸ“ -- ROUTE: Serve manifest.json dynamically for each site
129
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);
130
142
 
131
143
  // 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
132
- app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyPageHandler(), renderGovcyPage());
144
+ app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true),govcyLoadSubmissionData(),govcyPageHandler(), renderGovcyPage());
133
145
 
134
146
  // πŸ‘€ -- ROUTE: Add Review Page Route (BEFORE the dynamic route)
135
- app.get('/:siteId/review',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPageHandler(), renderGovcyPage());
147
+ app.get('/:siteId/review',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(),govcyLoadSubmissionData(), govcyReviewPageHandler(), renderGovcyPage());
136
148
 
137
149
  // βœ…πŸ“„ -- ROUTE: Add Success PDF Route (BEFORE the dynamic route)
138
150
  app.get('/:siteId/success/pdf',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(true), govcyPDFRender());
@@ -140,8 +152,17 @@ export default function initializeGovCyExpressService(){
140
152
  // βœ… -- ROUTE: Add Success Page Route (BEFORE the dynamic route)
141
153
  app.get('/:siteId/success',serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
142
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
+
143
161
  // πŸ“ -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
144
- app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyPageHandler(), renderGovcyPage());
162
+ app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
163
+
164
+ // βŒπŸ—ƒοΈπŸ“₯ -- ROUTE: Handle POST requests for delete file
165
+ app.post('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFileDeletePostHandler());
145
166
 
146
167
  // πŸ“₯ -- ROUTE: Handle POST requests for review page. The `submit` action
147
168
  app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
@@ -149,7 +170,6 @@ export default function initializeGovCyExpressService(){
149
170
  // πŸ‘€πŸ“₯ -- ROUTE: Handle POST requests (Form Submissions) based on siteId and pageUrl, using govcyFormsPostHandler middleware
150
171
  app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
151
172
 
152
-
153
173
  // post for /:siteId/review
154
174
 
155
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
+ ];