@gov-cy/govcy-express-services 0.2.16 β 1.0.0-alpha.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -4
- package/package.json +4 -2
- package/src/index.mjs +24 -4
- package/src/middleware/cyLoginAuth.mjs +8 -0
- package/src/middleware/govcyCsrf.mjs +15 -1
- package/src/middleware/govcyFileDeleteHandler.mjs +238 -0
- package/src/middleware/govcyFileUpload.mjs +36 -0
- package/src/middleware/govcyFileViewHandler.mjs +161 -0
- package/src/middleware/govcyFormsPostHandler.mjs +8 -2
- package/src/middleware/govcyHttpErrorHandler.mjs +4 -3
- package/src/middleware/govcyLoadSubmissionData.mjs +22 -0
- package/src/middleware/govcyPageHandler.mjs +5 -1
- package/src/public/js/govcyFiles.js +197 -0
- package/src/public/js/govcyForms.js +19 -8
- package/src/resources/govcyResources.mjs +69 -3
- package/src/utils/govcyApiDetection.mjs +17 -0
- package/src/utils/govcyApiRequest.mjs +30 -5
- package/src/utils/govcyApiResponse.mjs +31 -0
- package/src/utils/govcyConstants.mjs +5 -1
- package/src/utils/govcyDataLayer.mjs +71 -1
- package/src/utils/govcyExpressions.mjs +1 -1
- package/src/utils/govcyFormHandling.mjs +81 -5
- package/src/utils/govcyHandleFiles.mjs +307 -0
- package/src/utils/govcyLoadSubmissionDataAPIs.mjs +142 -0
- package/src/utils/govcySubmitData.mjs +62 -2
- package/src/utils/govcyTempSave.mjs +71 -0
- package/src/utils/govcyValidator.mjs +7 -0
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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
|
-
```
|
|
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.
|
|
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.
|
|
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
|
+
];
|