@gov-cy/govcy-express-services 1.4.3 → 1.6.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 +62 -222
- package/package.json +2 -2
- package/src/index.mjs +29 -29
- package/src/middleware/cyLoginAuth.mjs +81 -38
- package/src/middleware/govcyPageHandler.mjs +1 -0
- package/src/middleware/govcyReviewPostHandler.mjs +1 -1
- package/src/middleware/govcyUpdateMyDetails.mjs +1 -1
- package/src/utils/govcyFormHandling.mjs +72 -1
- package/src/utils/govcySubmitData.mjs +40 -12
- package/src/utils/govcyValidator.mjs +1 -0
package/README.md
CHANGED
|
@@ -30,6 +30,8 @@ The APIs used for submission, temporary save and file uploads are not part of th
|
|
|
30
30
|
- [✅ Best Practices](#-best-practices)
|
|
31
31
|
- [📦 Full installation guide](#-full-installation-guide)
|
|
32
32
|
- [🛠️ Usage](#%EF%B8%8F-usage)
|
|
33
|
+
- [🔑 Authentication Middleware](#-authentication-middleware)
|
|
34
|
+
- [cyLogin Access Policies](#cylogin-access-policies)
|
|
33
35
|
- [🧩 Dynamic services](#-dynamic-services)
|
|
34
36
|
- [Pages](#pages)
|
|
35
37
|
- [Form vs static pages](#form-vs-static-pages)
|
|
@@ -142,13 +144,67 @@ npm start
|
|
|
142
144
|
```
|
|
143
145
|
The server will start on `https://localhost:44319` (see [NOTES.md](NOTES.md#local-development) for more details on this).
|
|
144
146
|
|
|
145
|
-
### Authentication Middleware
|
|
147
|
+
### 🔑 Authentication Middleware
|
|
146
148
|
Authentication is handled via OpenID Connect using CY Login and is configured using environment variables. The middleware ensures users have valid sessions before accessing protected routes.
|
|
147
149
|
|
|
148
150
|
The CY Login tokens are used to also connect with the various APIs through [cyConnect](https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/74/CY-Connect), so make sure to include the correct `scope` when requesting for a [cyLogin client registration](https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/34/Developer-Guide).
|
|
149
151
|
|
|
150
152
|
The CY Login settings are configured in the `secrets/.env` file.
|
|
151
153
|
|
|
154
|
+
#### cyLogin Access Policies
|
|
155
|
+
|
|
156
|
+
Each service can specify which types of authenticated CY Login profiles are allowed to access it using the `site.cyLoginPolicies` property in its site configuration.
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
"cyLoginPolicies": ["naturalPerson", "legalPerson"]
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
##### Supported Policies
|
|
163
|
+
|
|
164
|
+
| Policy name | Description | Typical use |
|
|
165
|
+
| --------------- | ------------------------------------------------------------ | ---------------------------------------------------- |
|
|
166
|
+
| `naturalPerson` | Allows individual users (Cypriot citizens or foreign residents) who have a verified profile in the Civil Registry. Identified by `profile_type: "Individual"` and a 10-digit identifier starting with `00` (citizen) or `05` (foreigner). | Citizen-facing services, personal applications, etc. |
|
|
167
|
+
| `legalPerson` | Allows legal entities (companies, partnerships, organisations) with verified profiles in the Registrar of Companies. Identified by `profile_type: "Organisation"` and a `legal_unique_identifier`. | Business-facing services, company submissions, etc. |
|
|
168
|
+
|
|
169
|
+
##### How it works
|
|
170
|
+
|
|
171
|
+
- Access is granted if **any** of the listed policies pass.
|
|
172
|
+
- If the user’s CY Login profile does not match any of the allowed policies, the request is blocked.
|
|
173
|
+
|
|
174
|
+
##### Defaults
|
|
175
|
+
|
|
176
|
+
If `cyLoginPolicies` is omitted, the framework defaults to:
|
|
177
|
+
|
|
178
|
+
```json
|
|
179
|
+
"cyLoginPolicies": ["naturalPerson"]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
This maintains backward compatibility with existing services that only supported individual (civil registry) users.
|
|
183
|
+
|
|
184
|
+
##### Example
|
|
185
|
+
|
|
186
|
+
Allow both natural and legal persons:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
"site": {
|
|
190
|
+
"cyLoginPolicies": ["naturalPerson", "legalPerson"]
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Restrict access to natural persons only:
|
|
195
|
+
|
|
196
|
+
```json
|
|
197
|
+
"site": {
|
|
198
|
+
"cyLoginPolicies": ["naturalPerson"]
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
##### Notes
|
|
203
|
+
|
|
204
|
+
- This configuration applies globally to the service.
|
|
205
|
+
- Both `requireAuth` and `cyLoginPolicy` middlewares must be present on protected routes (automatically included by the default route setup).
|
|
206
|
+
|
|
207
|
+
|
|
152
208
|
### 🧩 Dynamic Services
|
|
153
209
|
Services are rendered dynamically using JSON templates stored in the `/data` folder. All the service configuration, pages, routes, and logic is stored in the JSON files. The service will load `data/:siteId.json` to get the form data when a user visits `/:siteId/:pageUrl`. Checkout the [express-service-shema.json](express-service-shema.json) and the example JSON structure of the **[test.json](data/test.json)** file for more details.
|
|
154
210
|
|
|
@@ -158,6 +214,7 @@ Here is an example JSON config:
|
|
|
158
214
|
{
|
|
159
215
|
"site": {
|
|
160
216
|
"id": "test",
|
|
217
|
+
"cyLoginPolicies": ["naturalPerson"], //<-- Allowed CY Login policies
|
|
161
218
|
"lang": "el", //<-- Default language
|
|
162
219
|
"languages": [ //<-- Supported languages
|
|
163
220
|
{
|
|
@@ -716,6 +773,8 @@ Here is an example JSON config:
|
|
|
716
773
|
Here are some details explaining the JSON structure:
|
|
717
774
|
|
|
718
775
|
- `site` object: Contains information about the site, including the site ID, language, and footer links. See [govcy-frontend-renderer](https://github.com/gov-cy/govcy-frontend-renderer/tree/main#site-and-page-meta-data-explained) for more details. Some fields that are only specific to the govcy-express-forms project are the following:
|
|
776
|
+
- `usesDSFSubmissionPlatform`: A boolean that indicates whether the service uses the DSF submission platform (transforms submission data as needed)
|
|
777
|
+
- `cyLoginPolicies`: which types of authenticated CY Login profiles are allowed to access the service
|
|
719
778
|
- `submissionDataVersion` : The submission data version,
|
|
720
779
|
- `rendererVersion` : The govcy-frontend-renderer version,
|
|
721
780
|
- `designSystemsVersion` : The govcy-design-system version,
|
|
@@ -2604,227 +2663,8 @@ To help back-end systems recognize the field as a file, the field's element name
|
|
|
2604
2663
|
If these endpoints are not defined in the service JSON, the file upload logic is skipped entirely.
|
|
2605
2664
|
Existing services will continue to work without modification.
|
|
2606
2665
|
|
|
2607
|
-
|
|
2608
|
-
The **Custom pages** feature allows developers to
|
|
2609
|
-
|
|
2610
|
-
These pages can collect data, display conditional content, and inject custom sections into the **review** and **email** stages of the service flow.
|
|
2611
|
-
|
|
2612
|
-
#### What do custom pages get out of the framework
|
|
2613
|
-
With the custom pages feature, developers can define **pages** and **routes** on an express **service**. On these pages the following apply out of the box:
|
|
2614
|
-
- Login requirement
|
|
2615
|
-
- User policy requirement
|
|
2616
|
-
- Any service eligibility checks
|
|
2617
|
-
- Csrf protection on POST
|
|
2618
|
-
- Generic error page on errors
|
|
2619
|
-
|
|
2620
|
-
The developers can also choose to:
|
|
2621
|
-
- store values in the session to be submitted via the submission API
|
|
2622
|
-
- define where and what the users sees in the `review` page
|
|
2623
|
-
- define errors regarding the custom page in the `review` page
|
|
2624
|
-
- define what the user receives the `email`
|
|
2625
|
-
|
|
2626
|
-
#### What the framework expects from custom pages
|
|
2627
|
-
|
|
2628
|
-
- Every custom page **must be defined** at startup using `defineCustomPages(app, siteId, pageUrl, ...)`.
|
|
2629
|
-
- Custom pages are **not** automatically discovered, they must be registered explicitly.
|
|
2630
|
-
- Each page must specify on **start up**:
|
|
2631
|
-
- `pageTitle`: multilingual object for display in review and email
|
|
2632
|
-
- `insertAfterPageUrl`: the page **after which** this custom page will appear
|
|
2633
|
-
- `summaryElements`, `errors`, and `email` are managed dynamically in session
|
|
2634
|
-
- **NOTE**: Usually a default `required` error must be defined on start up. This will ensure that the when the users try to submit in the “Check your answers” review page, they get an error message to complete an action.
|
|
2635
|
-
- `extraProps` extra property stored in the session to be used by the developers as they wish
|
|
2636
|
-
- During **runtime**, developers are responsible for setting:
|
|
2637
|
-
- `data`: data to include in submission, using the `setCustomPageData` function
|
|
2638
|
-
- `summaryElements`: what appears in the “Check your answers” review page, using the `setCustomPageSummaryElements` function
|
|
2639
|
-
- `email`: array of [`dsf-email-templates`](https://github.com/gov-cy/dsf-email-templates) components for the submission confirmation email, using the `setCustomPageEmail`
|
|
2640
|
-
- `errors`: errors that appear in the “Check your answers” review page. Developers can use the `addCustomPageError` function to add an error, or the `clearCustomPageErrors` to clear the errors
|
|
2641
|
-
#### Available methods
|
|
2642
|
-
|
|
2643
|
-
| Method | Purpose |
|
|
2644
|
-
| ------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------- |
|
|
2645
|
-
| `defineCustomPages(store, siteId, pageUrl, pageTitle, insertAfterSummary, insertAfterPageUrl, errors, summaryElements, summaryHtml, extraProps)` | Registers a custom page definition in `app`. Must be called once at startup. |
|
|
2646
|
-
| `getCustomPageDefinition(store, siteId, pageUrl)` | Retrieves the custom page definition for a given siteId and pageUrl |
|
|
2647
|
-
| `resetCustomPages(configStore, store, siteId)` | Resets per-session data from the global definitions. |
|
|
2648
|
-
| `setCustomPageData(store, siteId, pageUrl, dataObject)` | Sets or replaces the data object used during submission. |
|
|
2649
|
-
| `setCustomPageSummaryElements(store, siteId, pageUrl, summaryElements)` | Sets what appears in the review page summary. |
|
|
2650
|
-
| `clearCustomPageErrors(store, siteId, pageUrl)` | Clears validation errors. |
|
|
2651
|
-
| `addCustomPageError(store, siteId, pageUrl, errorId, errorTextObject)` | Adds a validation error. |
|
|
2652
|
-
| `setCustomPageEmail(store, siteId, pageUrl, arrayOfEmailObjects)` | Defines email sections for the confirmation email using `dsf-email-templates` components. |
|
|
2653
|
-
| `setCustomPageProperty(store, siteId, pageUrl, property, value, isDefinition = false)` | Sets a custom property on a given custom page definition or instance |
|
|
2654
|
-
| `getCustomPageProperty(store, siteId, pageUrl, property, isDefinition = false)` | Gets a custom property from a given custom page definition or instance. |
|
|
2655
|
-
|
|
2656
|
-
---
|
|
2657
|
-
##### Example
|
|
2658
|
-
|
|
2659
|
-
Below is a practical example of a custom **Declarations** page added to an existing service.
|
|
2660
|
-
|
|
2661
|
-
```js
|
|
2662
|
-
import initializeGovCyExpressService from '@gov-cy/govcy-express-services';
|
|
2663
|
-
import {
|
|
2664
|
-
defineCustomPages, resetCustomPages,
|
|
2665
|
-
clearCustomPageErrors, addCustomPageError,
|
|
2666
|
-
setCustomPageData, setCustomPageSummaryElements,
|
|
2667
|
-
setCustomPageEmail, getCustomPageProperty, setCustomPageProperty
|
|
2668
|
-
} from '@gov-cy/govcy-express-services/customPages';
|
|
2669
|
-
|
|
2670
|
-
// Initialize the service
|
|
2671
|
-
const service = initializeGovCyExpressService({
|
|
2672
|
-
beforeMount({ siteRoute, app }) {
|
|
2673
|
-
|
|
2674
|
-
// ==========================================================
|
|
2675
|
-
// 1️⃣ DEFINE GLOBAL CUSTOM PAGE CONFIGS (once per app)
|
|
2676
|
-
// ==========================================================
|
|
2677
|
-
defineCustomPages(
|
|
2678
|
-
app, // <-- using app as store (global config)
|
|
2679
|
-
"cso", // siteId
|
|
2680
|
-
"/cso/custom", // pageUrl
|
|
2681
|
-
{ en: "My custom section", el: "Προσαρμοσμένη ενότητα" }, // pageTitle
|
|
2682
|
-
"qualifications", // insertAfterPageUrl
|
|
2683
|
-
[ // errors
|
|
2684
|
-
{
|
|
2685
|
-
id: "custom-error",
|
|
2686
|
-
text: {
|
|
2687
|
-
en: "This is a custom error custom",
|
|
2688
|
-
el: "Αυτή ειναι ενα προσαρμοσμένη σφαλμα custom",
|
|
2689
|
-
}
|
|
2690
|
-
}
|
|
2691
|
-
],
|
|
2692
|
-
[ // summaryElements
|
|
2693
|
-
{
|
|
2694
|
-
key:
|
|
2695
|
-
{
|
|
2696
|
-
en: "Extra value",
|
|
2697
|
-
el: "Πρόσθετη τιμή"
|
|
2698
|
-
},
|
|
2699
|
-
value: []
|
|
2700
|
-
}
|
|
2701
|
-
],
|
|
2702
|
-
false,
|
|
2703
|
-
{ // other custom properties if needed like nextPage or initial data
|
|
2704
|
-
nextPage: "/cso/memberships", // custom property nextPage. Not needed by Express but useful for the custom logic
|
|
2705
|
-
data : // custom initial data. Useful when you need the data model to be standard or pre-populated
|
|
2706
|
-
{
|
|
2707
|
-
something: "",
|
|
2708
|
-
}
|
|
2709
|
-
}
|
|
2710
|
-
);
|
|
2711
|
-
|
|
2712
|
-
// ==========================================================
|
|
2713
|
-
// 2️⃣ MIDDLEWARE: SET UP SESSION DATA FROM GLOBAL DEFINITIONS
|
|
2714
|
-
// ==========================================================
|
|
2715
|
-
app.use((req, res, next) => {
|
|
2716
|
-
// Initialize session data for custom pages
|
|
2717
|
-
req.session.siteData ??= {};
|
|
2718
|
-
req.session.siteData["cso"] ??= {};
|
|
2719
|
-
|
|
2720
|
-
// Reset session copies if missing (first visit)
|
|
2721
|
-
if (!req.session.siteData["cso"].customPages) {
|
|
2722
|
-
resetCustomPages(app, req.session, "cso"); // 🔁 deep copy from app to session
|
|
2723
|
-
req.session.save((err) => {
|
|
2724
|
-
if (err) console.error("⚠️ Error initializing customPages:", err);
|
|
2725
|
-
});
|
|
2726
|
-
}
|
|
2727
|
-
|
|
2728
|
-
next();
|
|
2729
|
-
});
|
|
2730
|
-
|
|
2731
|
-
// ==========================================================
|
|
2732
|
-
// 3️⃣ CUSTOM ROUTES (still per-user)
|
|
2733
|
-
// ==========================================================
|
|
2734
|
-
|
|
2735
|
-
// GET `/cso/custom`
|
|
2736
|
-
siteRoute("cso", "get", "/cso/custom", (req, res) => {
|
|
2737
|
-
|
|
2738
|
-
// Render the custom page using the session data
|
|
2739
|
-
// It is important for POST to add the csrfToken and to pass on the route query parameter
|
|
2740
|
-
res.send(`<form action="/cso/custom${(req.query.route === "review")?"?route=review":""}" method="post">
|
|
2741
|
-
custom for ${req.params.siteId}
|
|
2742
|
-
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
|
|
2743
|
-
<button type="submit">Submit custom page</button>
|
|
2744
|
-
</form>`);
|
|
2745
|
-
|
|
2746
|
-
});
|
|
2747
|
-
|
|
2748
|
-
// POST `/cso/custom`
|
|
2749
|
-
siteRoute("cso", "post", "/cso/custom", (req, res) => {
|
|
2750
|
-
|
|
2751
|
-
// Update custom page `data` dynamically`
|
|
2752
|
-
setCustomPageData(req.session, "cso", "/cso/custom", {
|
|
2753
|
-
something: "123",
|
|
2754
|
-
});
|
|
2755
|
-
|
|
2756
|
-
// Update `summary elements` dynamically (example)
|
|
2757
|
-
setCustomPageSummaryElements(req.session, "cso", "/cso/custom",
|
|
2758
|
-
[
|
|
2759
|
-
{
|
|
2760
|
-
key: {
|
|
2761
|
-
en: "Extra value",
|
|
2762
|
-
el: "Πρόσθετη τιμή"
|
|
2763
|
-
},
|
|
2764
|
-
value: [
|
|
2765
|
-
{
|
|
2766
|
-
element: "textElement",
|
|
2767
|
-
params: {
|
|
2768
|
-
text: {
|
|
2769
|
-
en: "123 Changed",
|
|
2770
|
-
el: "123 Αλλάχθηκε"
|
|
2771
|
-
},
|
|
2772
|
-
type: "span",
|
|
2773
|
-
showNewLine: true,
|
|
2774
|
-
}
|
|
2775
|
-
}
|
|
2776
|
-
]
|
|
2777
|
-
}
|
|
2778
|
-
]);
|
|
2779
|
-
|
|
2780
|
-
// Update the custom page `email`
|
|
2781
|
-
setCustomPageEmail(req.session, "cso", "/cso/custom", [
|
|
2782
|
-
{
|
|
2783
|
-
component: "bodyKeyValue",
|
|
2784
|
-
params: {
|
|
2785
|
-
type: "ul",
|
|
2786
|
-
items: [
|
|
2787
|
-
{ key: "Extra value", value: "123" },
|
|
2788
|
-
{ key: "Priority level", value: "High" },
|
|
2789
|
-
],
|
|
2790
|
-
},
|
|
2791
|
-
}
|
|
2792
|
-
]);
|
|
2793
|
-
|
|
2794
|
-
// clear any previous errors
|
|
2795
|
-
clearCustomPageErrors(req.session, "cso", "/cso/custom");
|
|
2796
|
-
|
|
2797
|
-
// Add a custom error
|
|
2798
|
-
// addCustomPageError(req.session, "cso", "/cso/custom", {
|
|
2799
|
-
// id: "custom-error",
|
|
2800
|
-
// text: {
|
|
2801
|
-
// en: "This is a custom error custom",
|
|
2802
|
-
// el: "Αυτή ειναι ενα προσαρμοσμένη σφαλμα custom",
|
|
2803
|
-
// }
|
|
2804
|
-
// });
|
|
2805
|
-
|
|
2806
|
-
//if route is review, redirect to review page else go to nextPage property
|
|
2807
|
-
if (req.query.route === "review") {
|
|
2808
|
-
res.redirect("/cso/review");
|
|
2809
|
-
return;
|
|
2810
|
-
} else {
|
|
2811
|
-
//example of using custom property nextPage
|
|
2812
|
-
res.redirect(getCustomPageProperty(req.session, "cso", "/cso/custom", "nextPage", false));
|
|
2813
|
-
return;
|
|
2814
|
-
}
|
|
2815
|
-
});
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
// ==========================================================
|
|
2819
|
-
},
|
|
2820
|
-
});
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
// Start the server
|
|
2824
|
-
service.startServer();
|
|
2825
|
-
|
|
2826
|
-
```
|
|
2827
|
-
|
|
2666
|
+
### ✨ Custom pages feature
|
|
2667
|
+
The **Custom pages** feature allows developers to code service-specific custom pages that exist outside the standard Express Services JSON configuration. More details at [Custom-pages.md](docs/Custom-pages.md)
|
|
2828
2668
|
|
|
2829
2669
|
### 🛣️ Routes
|
|
2830
2670
|
The project uses express.js to serve the following routes:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gov-cy/govcy-express-services",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.6.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",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
"coverage:badge": "coverage-badges --output ./coverage-badges.svg && npm run coverage:copy"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@gov-cy/dsf-email-templates": "^2.1.
|
|
59
|
+
"@gov-cy/dsf-email-templates": "^2.1.11",
|
|
60
60
|
"@gov-cy/govcy-frontend-renderer": "^1.26.0",
|
|
61
61
|
"axios": "^1.9.0",
|
|
62
62
|
"cookie-parser": "^1.4.7",
|
package/src/index.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { govcyCsrfMiddleware } from './middleware/govcyCsrf.mjs';
|
|
|
18
18
|
import { govcySessionData } from './middleware/govcySessionData.mjs';
|
|
19
19
|
import { govcyHttpErrorHandler } from './middleware/govcyHttpErrorHandler.mjs';
|
|
20
20
|
import { govcyLanguageMiddleware } from './middleware/govcyLanguageMiddleware.mjs';
|
|
21
|
-
import { requireAuth,
|
|
21
|
+
import { requireAuth, cyLoginPolicy, handleLoginRoute, handleSigninOidc, handleLogout } from './middleware/cyLoginAuth.mjs';
|
|
22
22
|
import { serviceConfigDataMiddleware } from './middleware/govcyConfigSiteData.mjs';
|
|
23
23
|
import { govcyManifestHandler } from './middleware/govcyManifestHandler.mjs';
|
|
24
24
|
import { govcyRoutePageHandler } from './middleware/govcyRoutePageHandler.mjs';
|
|
@@ -103,7 +103,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
103
103
|
// 🛠️ Debugging routes -----------------------------------------------------
|
|
104
104
|
// 🙍🏻♂️ -- ROUTE: Debugging route Protected Route
|
|
105
105
|
// if (!isProdOrStaging()) {
|
|
106
|
-
// app.get('/user', requireAuth,
|
|
106
|
+
// app.get('/user', requireAuth, cyLoginPolicy, (req, res) => {
|
|
107
107
|
// res.send(`
|
|
108
108
|
// User name: ${req.session.user.name}
|
|
109
109
|
// <br> Sub: ${req.session.user.sub}
|
|
@@ -139,7 +139,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
139
139
|
app.post('/apis/:siteId/:pageUrl/upload',
|
|
140
140
|
serviceConfigDataMiddleware,
|
|
141
141
|
requireAuth, // UNCOMMENT
|
|
142
|
-
|
|
142
|
+
cyLoginPolicy, // UNCOMMENT
|
|
143
143
|
govcyServiceEligibilityHandler(true), // UNCOMMENT
|
|
144
144
|
govcyFileUpload);
|
|
145
145
|
|
|
@@ -147,7 +147,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
147
147
|
app.post('/apis/:siteId/:pageUrl/multiple/add/upload',
|
|
148
148
|
serviceConfigDataMiddleware,
|
|
149
149
|
requireAuth, // UNCOMMENT
|
|
150
|
-
|
|
150
|
+
cyLoginPolicy, // UNCOMMENT
|
|
151
151
|
govcyServiceEligibilityHandler(true), // UNCOMMENT
|
|
152
152
|
govcyFileUpload
|
|
153
153
|
);
|
|
@@ -176,7 +176,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
176
176
|
injectSiteId,
|
|
177
177
|
serviceConfigDataMiddleware,
|
|
178
178
|
requireAuth,
|
|
179
|
-
|
|
179
|
+
cyLoginPolicy,
|
|
180
180
|
govcyServiceEligibilityHandler(),
|
|
181
181
|
...handlers,
|
|
182
182
|
];
|
|
@@ -196,7 +196,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
196
196
|
app.post('/apis/:siteId/:pageUrl/multiple/edit/:index/upload',
|
|
197
197
|
serviceConfigDataMiddleware,
|
|
198
198
|
requireAuth, // UNCOMMENT
|
|
199
|
-
|
|
199
|
+
cyLoginPolicy, // UNCOMMENT
|
|
200
200
|
govcyServiceEligibilityHandler(true), // UNCOMMENT
|
|
201
201
|
govcyFileUpload
|
|
202
202
|
);
|
|
@@ -205,7 +205,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
205
205
|
app.get('/:siteId/:pageUrl/multiple/add/view-file/:elementName',
|
|
206
206
|
serviceConfigDataMiddleware,
|
|
207
207
|
requireAuth,
|
|
208
|
-
|
|
208
|
+
cyLoginPolicy,
|
|
209
209
|
govcyServiceEligibilityHandler(true),
|
|
210
210
|
govcyFileViewHandler());
|
|
211
211
|
|
|
@@ -213,7 +213,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
213
213
|
app.get('/:siteId/:pageUrl/multiple/add/delete-file/:elementName',
|
|
214
214
|
serviceConfigDataMiddleware,
|
|
215
215
|
requireAuth,
|
|
216
|
-
|
|
216
|
+
cyLoginPolicy,
|
|
217
217
|
govcyServiceEligibilityHandler(),
|
|
218
218
|
govcyLoadSubmissionData(),
|
|
219
219
|
govcyFileDeletePageHandler(),
|
|
@@ -224,7 +224,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
224
224
|
app.get('/:siteId/:pageUrl/multiple/edit/:index/delete-file/:elementName',
|
|
225
225
|
serviceConfigDataMiddleware,
|
|
226
226
|
requireAuth,
|
|
227
|
-
|
|
227
|
+
cyLoginPolicy,
|
|
228
228
|
govcyServiceEligibilityHandler(),
|
|
229
229
|
govcyLoadSubmissionData(),
|
|
230
230
|
govcyFileDeletePageHandler(),
|
|
@@ -236,7 +236,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
236
236
|
app.post('/:siteId/:pageUrl/multiple/add/delete-file/:elementName',
|
|
237
237
|
serviceConfigDataMiddleware,
|
|
238
238
|
requireAuth,
|
|
239
|
-
|
|
239
|
+
cyLoginPolicy,
|
|
240
240
|
govcyServiceEligibilityHandler(true),
|
|
241
241
|
govcyFileDeletePostHandler()
|
|
242
242
|
);
|
|
@@ -245,7 +245,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
245
245
|
app.post('/:siteId/:pageUrl/multiple/edit/:index/delete-file/:elementName',
|
|
246
246
|
serviceConfigDataMiddleware,
|
|
247
247
|
requireAuth,
|
|
248
|
-
|
|
248
|
+
cyLoginPolicy,
|
|
249
249
|
govcyServiceEligibilityHandler(true),
|
|
250
250
|
govcyFileDeletePostHandler()
|
|
251
251
|
);
|
|
@@ -255,33 +255,33 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
255
255
|
app.get('/:siteId/:pageUrl/multiple/edit/:index/view-file/:elementName',
|
|
256
256
|
serviceConfigDataMiddleware,
|
|
257
257
|
requireAuth,
|
|
258
|
-
|
|
258
|
+
cyLoginPolicy,
|
|
259
259
|
govcyServiceEligibilityHandler(true),
|
|
260
260
|
govcyFileViewHandler());
|
|
261
261
|
|
|
262
262
|
// 🏠 -- ROUTE: Handle route with only siteId (/:siteId or /:siteId/)
|
|
263
|
-
app.get('/:siteId', serviceConfigDataMiddleware, requireAuth,
|
|
263
|
+
app.get('/:siteId', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
|
|
264
264
|
|
|
265
265
|
// 👀 -- ROUTE: Add Review Page Route (BEFORE the dynamic route)
|
|
266
|
-
app.get('/:siteId/review', serviceConfigDataMiddleware, requireAuth,
|
|
266
|
+
app.get('/:siteId/review', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyReviewPageHandler(), renderGovcyPage());
|
|
267
267
|
|
|
268
268
|
// ✅📄 -- ROUTE: Add Success PDF Route (BEFORE the dynamic route)
|
|
269
|
-
app.get('/:siteId/success/pdf', serviceConfigDataMiddleware, requireAuth,
|
|
269
|
+
app.get('/:siteId/success/pdf', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(true), govcyPDFRender());
|
|
270
270
|
|
|
271
271
|
// ✅ -- ROUTE: Add Success Page Route (BEFORE the dynamic route)
|
|
272
|
-
app.get('/:siteId/success', serviceConfigDataMiddleware, requireAuth,
|
|
272
|
+
app.get('/:siteId/success', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
|
|
273
273
|
|
|
274
274
|
// 👀🗃️ -- ROUTE: View file (BEFORE the dynamic route)
|
|
275
|
-
app.get('/:siteId/:pageUrl/view-file/:elementName', serviceConfigDataMiddleware, requireAuth,
|
|
275
|
+
app.get('/:siteId/:pageUrl/view-file/:elementName', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileViewHandler());
|
|
276
276
|
|
|
277
277
|
// ❌🗃️ -- ROUTE: Delete file (BEFORE the dynamic route)
|
|
278
|
-
app.get('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth,
|
|
278
|
+
app.get('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileDeletePageHandler(), renderGovcyPage());
|
|
279
279
|
|
|
280
280
|
// ➕ -- ROUTE: Add item page (BEFORE the generic dynamic route)
|
|
281
281
|
app.get('/:siteId/:pageUrl/multiple/add',
|
|
282
282
|
serviceConfigDataMiddleware,
|
|
283
283
|
requireAuth,
|
|
284
|
-
|
|
284
|
+
cyLoginPolicy,
|
|
285
285
|
govcyServiceEligibilityHandler(true),
|
|
286
286
|
govcyLoadSubmissionData(),
|
|
287
287
|
govcyMultipleThingsAddHandler(),
|
|
@@ -293,7 +293,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
293
293
|
app.post('/:siteId/:pageUrl/multiple/add',
|
|
294
294
|
serviceConfigDataMiddleware,
|
|
295
295
|
requireAuth,
|
|
296
|
-
|
|
296
|
+
cyLoginPolicy,
|
|
297
297
|
govcyServiceEligibilityHandler(true),
|
|
298
298
|
govcyMultipleThingsAddPostHandler()
|
|
299
299
|
);
|
|
@@ -302,7 +302,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
302
302
|
app.get('/:siteId/:pageUrl/multiple/edit/:index',
|
|
303
303
|
serviceConfigDataMiddleware,
|
|
304
304
|
requireAuth,
|
|
305
|
-
|
|
305
|
+
cyLoginPolicy,
|
|
306
306
|
govcyServiceEligibilityHandler(true),
|
|
307
307
|
govcyLoadSubmissionData(),
|
|
308
308
|
govcyMultipleThingsEditHandler(),
|
|
@@ -313,7 +313,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
313
313
|
app.post('/:siteId/:pageUrl/multiple/edit/:index',
|
|
314
314
|
serviceConfigDataMiddleware,
|
|
315
315
|
requireAuth,
|
|
316
|
-
|
|
316
|
+
cyLoginPolicy,
|
|
317
317
|
govcyServiceEligibilityHandler(true),
|
|
318
318
|
govcyMultipleThingsEditPostHandler()
|
|
319
319
|
);
|
|
@@ -322,7 +322,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
322
322
|
app.get('/:siteId/:pageUrl/multiple/delete/:index',
|
|
323
323
|
serviceConfigDataMiddleware,
|
|
324
324
|
requireAuth,
|
|
325
|
-
|
|
325
|
+
cyLoginPolicy,
|
|
326
326
|
govcyServiceEligibilityHandler(),
|
|
327
327
|
govcyLoadSubmissionData(),
|
|
328
328
|
govcyMultipleThingsDeletePageHandler(),
|
|
@@ -333,7 +333,7 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
333
333
|
app.post('/:siteId/:pageUrl/multiple/delete/:index',
|
|
334
334
|
serviceConfigDataMiddleware,
|
|
335
335
|
requireAuth,
|
|
336
|
-
|
|
336
|
+
cyLoginPolicy,
|
|
337
337
|
govcyServiceEligibilityHandler(true),
|
|
338
338
|
govcyMultipleThingsDeletePostHandler()
|
|
339
339
|
);
|
|
@@ -344,22 +344,22 @@ export default function initializeGovCyExpressService(opts = {}) {
|
|
|
344
344
|
app.post('/:siteId/:pageUrl/update-my-details-response',
|
|
345
345
|
serviceConfigDataMiddleware,
|
|
346
346
|
requireAuth,
|
|
347
|
-
|
|
347
|
+
cyLoginPolicy,
|
|
348
348
|
govcyServiceEligibilityHandler(true),
|
|
349
349
|
govcyUpdateMyDetailsPostHandler());
|
|
350
350
|
// ----- `updateMyDetails` handling
|
|
351
351
|
|
|
352
352
|
// 📝 -- ROUTE: Dynamic route to render pages based on siteId and pageUrl, using govcyPageHandler middleware
|
|
353
|
-
app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth,
|
|
353
|
+
app.get('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
|
|
354
354
|
|
|
355
355
|
// ❌🗃️📥 -- ROUTE: Handle POST requests for delete file
|
|
356
|
-
app.post('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth,
|
|
356
|
+
app.post('/:siteId/:pageUrl/delete-file/:elementName', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyFileDeletePostHandler());
|
|
357
357
|
|
|
358
358
|
// 📥 -- ROUTE: Handle POST requests for review page. The `submit` action
|
|
359
|
-
app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth,
|
|
359
|
+
app.post('/:siteId/review', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
|
|
360
360
|
|
|
361
361
|
// 👀📥 -- ROUTE: Handle POST requests (Form Submissions) based on siteId and pageUrl, using govcyFormsPostHandler middleware
|
|
362
|
-
app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth,
|
|
362
|
+
app.post('/:siteId/:pageUrl', serviceConfigDataMiddleware, requireAuth, cyLoginPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
|
|
363
363
|
|
|
364
364
|
// post for /:siteId/review
|
|
365
365
|
|
|
@@ -7,10 +7,10 @@
|
|
|
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";
|
|
10
|
+
import { errorResponse } from "../utils/govcyApiResponse.mjs";
|
|
11
11
|
import { isApiRequest } from '../utils/govcyApiDetection.mjs';
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
/**
|
|
15
15
|
* Middleware to check if the user is authenticated. If not, redirect to the login page.
|
|
16
16
|
*
|
|
@@ -33,39 +33,7 @@ export function requireAuth(req, res, next) {
|
|
|
33
33
|
next();
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
* Middleware to enforce natural person policy. If the user is not a natural person, return a 403 error.
|
|
38
|
-
*
|
|
39
|
-
* @param {object} req The request object
|
|
40
|
-
* @param {object} res The response object
|
|
41
|
-
* @param {object} next The next middleware function
|
|
42
|
-
*/
|
|
43
|
-
export function naturalPersonPolicy(req, res, next) {
|
|
44
|
-
// // allow only natural persons with approved profiles
|
|
45
|
-
// if (req.session.user.profile_type == 'Individual' && req.session.user.unique_identifier) {
|
|
46
|
-
// next();
|
|
47
|
-
// } else {
|
|
48
|
-
// return handleMiddlewareError("🚨 Access Denied: natural person policy not met.", 403, next);
|
|
49
|
-
// }
|
|
50
|
-
// https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/42/For-Cyprus-Natural-or-Legal-person
|
|
51
|
-
const { profile_type, unique_identifier } = req.session.user || {};
|
|
52
|
-
// Allow only natural persons with approved profiles
|
|
53
|
-
if (profile_type === 'Individual' && unique_identifier) {
|
|
54
|
-
|
|
55
|
-
// Validate Cypriot Citizen (starts with "00" and is 10 characters long)
|
|
56
|
-
if (unique_identifier.startsWith('00') && unique_identifier.length === 10) {
|
|
57
|
-
return next();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Validate Foreigner with ARN (starts with "05" and is 10 characters long)
|
|
61
|
-
if (unique_identifier.startsWith('05') && unique_identifier.length === 10) {
|
|
62
|
-
return next();
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
// Deny access if validation fails
|
|
67
|
-
return handleMiddlewareError("🚨 Access Denied: natural person policy not met.", 403, next);
|
|
68
|
-
}
|
|
36
|
+
/* c8 ignore start */
|
|
69
37
|
|
|
70
38
|
/**
|
|
71
39
|
* Middleware to handle the login route. Redirects the user to the login URL.
|
|
@@ -107,11 +75,11 @@ export function handleSigninOidc() {
|
|
|
107
75
|
// Redirect to the stored URL after login or fallback to '/'
|
|
108
76
|
const redirectUrl = req.session.redirectAfterLogin || '/';
|
|
109
77
|
// Clean up session for redirect after login
|
|
110
|
-
delete req.session.redirectAfterLogin;
|
|
78
|
+
delete req.session.redirectAfterLogin;
|
|
111
79
|
// Redirect to the stored URL
|
|
112
80
|
res.redirect(redirectUrl);
|
|
113
81
|
} catch (error) {
|
|
114
|
-
logger.debug('Token exchange failed:', error,req);
|
|
82
|
+
logger.debug('Token exchange failed:', error, req);
|
|
115
83
|
res.status(500).send('Authentication failed');
|
|
116
84
|
}
|
|
117
85
|
}
|
|
@@ -138,4 +106,79 @@ export function handleLogout() {
|
|
|
138
106
|
});
|
|
139
107
|
};
|
|
140
108
|
}
|
|
141
|
-
/* c8 ignore end */
|
|
109
|
+
/* c8 ignore end */
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
/************************************************************************/
|
|
113
|
+
/**
|
|
114
|
+
* Middleware to enforce natural person policy. If the user is not a verified natural person, return a false.
|
|
115
|
+
*
|
|
116
|
+
* @param {object} req The request object
|
|
117
|
+
*/
|
|
118
|
+
export function naturalPersonPolicy(req) {
|
|
119
|
+
// https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/42/For-Cyprus-Natural-or-Legal-person
|
|
120
|
+
const { profile_type, unique_identifier } = req.session.user || {};
|
|
121
|
+
// Allow only natural persons with approved profiles
|
|
122
|
+
if (profile_type === 'Individual' && unique_identifier) {
|
|
123
|
+
|
|
124
|
+
// Validate Cypriot Citizen (starts with "00" and is 10 characters long)
|
|
125
|
+
if (unique_identifier.startsWith('00') && unique_identifier.length === 10) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Validate Foreigner with ARN (starts with "05" and is 10 characters long)
|
|
130
|
+
if (unique_identifier.startsWith('05') && unique_identifier.length === 10) {
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Deny access if validation fails
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** * Middleware to enforce legal person policy. If the user is not a verified legal person, return false.
|
|
140
|
+
*
|
|
141
|
+
* @param {object} req The request object
|
|
142
|
+
*/
|
|
143
|
+
export function legalPersonPolicy(req) {
|
|
144
|
+
// https://dev.azure.com/cyprus-gov-cds/Documentation/_wiki/wikis/Documentation/42/For-Cyprus-Natural-or-Legal-person
|
|
145
|
+
const { profile_type, legal_unique_identifier } = req.session.user || {};
|
|
146
|
+
// Allow only legal persons with approved profiles
|
|
147
|
+
if (profile_type === 'Organisation' && legal_unique_identifier) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Deny access if validation fails
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const policyRegistry = {
|
|
156
|
+
naturalPerson: naturalPersonPolicy,
|
|
157
|
+
legalPerson: legalPersonPolicy,
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
export function cyLoginPolicy(req, res, next) {
|
|
161
|
+
// Check what is allowed in the service configuration
|
|
162
|
+
const allowed = req?.serviceData?.site?.cyLoginPolicies || ["naturalPerson"];
|
|
163
|
+
|
|
164
|
+
// Check each policy in the allowed list
|
|
165
|
+
for (const name of allowed) {
|
|
166
|
+
const policy = policyRegistry[name];
|
|
167
|
+
// Skip if the policy is not registered
|
|
168
|
+
if (!policy) {
|
|
169
|
+
console.warn(`🚨 Unknown policy: ${name}`);
|
|
170
|
+
continue
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// 🚨 Strict mode: let errors throw naturally if data is malformed
|
|
174
|
+
const passed = policy(req);
|
|
175
|
+
if (passed) return next();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return handleMiddlewareError(
|
|
179
|
+
"🚨 Access Denied: none of the allowed CY Login policies matched.",
|
|
180
|
+
403,
|
|
181
|
+
next
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
/************************************************************************/
|
|
@@ -26,6 +26,7 @@ export function govcyPageHandler() {
|
|
|
26
26
|
logger.debug(`No pageUrl provided for siteId: ${siteId}`, req);
|
|
27
27
|
// Example: Redirect to a default page or load a homepage
|
|
28
28
|
pageUrl = "index"; // Change "index" to whatever makes sense for your service
|
|
29
|
+
req.params.pageUrl = pageUrl; // Update req.params to reflect the new pageUrl
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
// 🔍 Find the page by pageUrl
|
|
@@ -184,7 +184,7 @@ export function govcyReviewPostHandler() {
|
|
|
184
184
|
const submissionData = prepareSubmissionData(req, siteId, service);
|
|
185
185
|
|
|
186
186
|
// Prepare submission data for API
|
|
187
|
-
const submissionDataAPI = prepareSubmissionDataAPI(submissionData);
|
|
187
|
+
const submissionDataAPI = prepareSubmissionDataAPI(submissionData, service);
|
|
188
188
|
|
|
189
189
|
logger.debug("Prepared submission data for API:", submissionDataAPI);
|
|
190
190
|
|
|
@@ -497,7 +497,7 @@ export function govcyUpdateMyDetailsPostHandler() {
|
|
|
497
497
|
// 🔄 User chose to update their details externally
|
|
498
498
|
const redirectUrl = constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, returnUrl);
|
|
499
499
|
logger.info("User opted to update details externally", {
|
|
500
|
-
userId: user.
|
|
500
|
+
userId: user.sub,
|
|
501
501
|
redirectUrl
|
|
502
502
|
});
|
|
503
503
|
return res.redirect(redirectUrl);
|
|
@@ -133,7 +133,7 @@ export function populateFormData(
|
|
|
133
133
|
// Delete link (preserve ?route=review if present)
|
|
134
134
|
element.params.deleteHref = `${basePath}/delete-file/${fieldName}${(routeParam) ? `?route=${routeParam}` : ''}`
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
|
|
137
137
|
} else {
|
|
138
138
|
// TODO: Ask Andreas how to handle empty file inputs
|
|
139
139
|
element.params.value = "";
|
|
@@ -295,3 +295,74 @@ export function getFormData(elements, formData, store = {}, siteId = "", pageUrl
|
|
|
295
295
|
|
|
296
296
|
return filteredData;
|
|
297
297
|
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get empty form data for multipleThings elements.
|
|
301
|
+
* Used to fill in empty multipleThings pages with the correct structure
|
|
302
|
+
*
|
|
303
|
+
* @param {object|Array} pageOrElements The page or elements of a conditional radio
|
|
304
|
+
* @param {object} emptyObject The object to populate with empty values
|
|
305
|
+
* @returns {object} An object with empty values for each form element
|
|
306
|
+
*/
|
|
307
|
+
export function getMultipleThingsEmptyFormData(pageOrElements, emptyObject = {}) {
|
|
308
|
+
// Determine if we're given a full page or just an array of elements
|
|
309
|
+
let elements = [];
|
|
310
|
+
if (Array.isArray(pageOrElements)) {
|
|
311
|
+
elements = pageOrElements; // recursion case
|
|
312
|
+
} else if (pageOrElements?.pageTemplate?.sections) {
|
|
313
|
+
// Deep copy to avoid modifying the original
|
|
314
|
+
const pageTemplateCopy = JSON.parse(JSON.stringify(pageOrElements.pageTemplate));
|
|
315
|
+
// Find the first form element in sections
|
|
316
|
+
for (const section of pageTemplateCopy.sections) {
|
|
317
|
+
const form = section.elements?.find(el => el.element === "form");
|
|
318
|
+
if (form) {
|
|
319
|
+
elements = form?.params?.elements || [];
|
|
320
|
+
break;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} else {
|
|
324
|
+
// No valid elements
|
|
325
|
+
return emptyObject;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Iterate through elements in order (like getFormData)
|
|
329
|
+
elements.forEach(element => {
|
|
330
|
+
const { name } = element.params || {};
|
|
331
|
+
|
|
332
|
+
if (ALLOWED_FORM_ELEMENTS.includes(element.element) && name) {
|
|
333
|
+
// Handle different element types
|
|
334
|
+
if (["checkboxes", "radios", "select"].includes(element.element)) {
|
|
335
|
+
emptyObject[name] = "";
|
|
336
|
+
|
|
337
|
+
// Handle conditional radios (same recursion pattern as getFormData)
|
|
338
|
+
if (element.element === "radios" && Array.isArray(element.params?.items)) {
|
|
339
|
+
element.params.items.forEach(item => {
|
|
340
|
+
if (item.conditionalElements) {
|
|
341
|
+
Object.assign(
|
|
342
|
+
emptyObject,
|
|
343
|
+
getMultipleThingsEmptyFormData(item.conditionalElements, {})
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Handle dateInput (single name, per your clarification)
|
|
351
|
+
else if (element.element === "dateInput") {
|
|
352
|
+
emptyObject[name] = "";
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Handle fileInput (suffix Attachment, per submission schema)
|
|
356
|
+
else if (element.element === "fileInput") {
|
|
357
|
+
emptyObject[`${name}Attachment`] = "";
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Handle all other elements (textInput, textArea, datePicker, etc.)
|
|
361
|
+
else {
|
|
362
|
+
emptyObject[name] = "";
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
return emptyObject;
|
|
368
|
+
}
|
|
@@ -4,7 +4,8 @@ import * as dataLayer from "./govcyDataLayer.mjs";
|
|
|
4
4
|
import { DSFEmailRenderer } from '@gov-cy/dsf-email-templates';
|
|
5
5
|
import { ALLOWED_FORM_ELEMENTS } from "./govcyConstants.mjs";
|
|
6
6
|
import { evaluatePageConditions } from "./govcyExpressions.mjs";
|
|
7
|
-
import { getPageConfigData } from "./govcyLoadConfigData.mjs";
|
|
7
|
+
import { getServiceConfigData, getPageConfigData } from "./govcyLoadConfigData.mjs";
|
|
8
|
+
import { getMultipleThingsEmptyFormData } from "./govcyFormHandling.mjs";
|
|
8
9
|
import { logger } from "./govcyLogger.mjs";
|
|
9
10
|
import { createUmdManualPageTemplate } from "../middleware/govcyUpdateMyDetails.mjs"
|
|
10
11
|
import nunjucks from "nunjucks";
|
|
@@ -252,21 +253,48 @@ export function prepareSubmissionData(req, siteId, service) {
|
|
|
252
253
|
/**
|
|
253
254
|
* Prepares the submission data for the API, stringifying all relevant fields.
|
|
254
255
|
*
|
|
255
|
-
* @param {object} data data prepared by `prepareSubmissionData
|
|
256
|
+
* @param {object} data data prepared by `prepareSubmissionData`\
|
|
257
|
+
* @param {object} service service config data
|
|
256
258
|
* @returns {object} The API-ready submission data object with all fields as strings
|
|
257
259
|
*/
|
|
258
|
-
export function prepareSubmissionDataAPI(data) {
|
|
260
|
+
export function prepareSubmissionDataAPI(data, service) {
|
|
261
|
+
|
|
262
|
+
//deep copy data to avoid mutating the original
|
|
263
|
+
let dataObj = JSON.parse(JSON.stringify(data));
|
|
264
|
+
// get site?.submissionAPIEndpoint.isDSFSubmissionPlatform
|
|
265
|
+
const isDSFSubmissionPlatform = service?.site?.usesDSFSubmissionPlatform || false;
|
|
266
|
+
|
|
267
|
+
// If DSF Submission Platform, ensure submissionData is an array
|
|
268
|
+
// this is intended for multipleThings pages only
|
|
269
|
+
if (isDSFSubmissionPlatform) {
|
|
270
|
+
// loop through submissionData
|
|
271
|
+
for (const [key, value] of Object.entries(dataObj.submissionData || {})) {
|
|
272
|
+
// check if the value is an empty array
|
|
273
|
+
if (Array.isArray(value) && value.length === 0) {
|
|
274
|
+
// get the pageConfigData for the page
|
|
275
|
+
try {
|
|
276
|
+
const page = getPageConfigData(service, key);
|
|
277
|
+
let pageEmptyData = getMultipleThingsEmptyFormData(page);
|
|
278
|
+
//replace the dataObj.submissionData[key] with the empty data
|
|
279
|
+
dataObj.submissionData[key] = [pageEmptyData];
|
|
280
|
+
|
|
281
|
+
} catch (error) {
|
|
282
|
+
logger.error('Error getting pageConfigData:', key);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
259
287
|
|
|
260
288
|
return {
|
|
261
|
-
submissionUsername: String(
|
|
262
|
-
submissionEmail: String(
|
|
263
|
-
submissionData: JSON.stringify(
|
|
264
|
-
submissionDataVersion: String(
|
|
265
|
-
printFriendlyData: JSON.stringify(
|
|
266
|
-
rendererData: JSON.stringify(
|
|
267
|
-
rendererVersion: String(
|
|
268
|
-
designSystemsVersion: String(
|
|
269
|
-
service: JSON.stringify(
|
|
289
|
+
submissionUsername: String(dataObj.submissionUsername ?? ""),
|
|
290
|
+
submissionEmail: String(dataObj.submissionEmail ?? ""),
|
|
291
|
+
submissionData: JSON.stringify(dataObj.submissionData ?? {}),
|
|
292
|
+
submissionDataVersion: String(dataObj.submissionDataVersion ?? ""),
|
|
293
|
+
printFriendlyData: JSON.stringify(dataObj.printFriendlyData ?? []),
|
|
294
|
+
rendererData: JSON.stringify(dataObj.rendererData ?? {}),
|
|
295
|
+
rendererVersion: String(dataObj.rendererVersion ?? ""),
|
|
296
|
+
designSystemsVersion: String(dataObj.designSystemsVersion ?? ""),
|
|
297
|
+
service: JSON.stringify(dataObj.service ?? {})
|
|
270
298
|
};
|
|
271
299
|
}
|
|
272
300
|
|
|
@@ -28,6 +28,7 @@ function validateValue(value, rules) {
|
|
|
28
28
|
alpha: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff\s]+$/.test(val),
|
|
29
29
|
alphaNum: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff0-9\s]+$/.test(val),
|
|
30
30
|
noSpecialChars: (val) => /^([0-9]|[A-Z]|[a-z]|[α-ω]|[Α-Ω]|[,]|[.]|[-]|[(]|[)]|[?]|[!]|[;]|[:]|[\n]|[\r]|[ _]|[\u0370-\u03ff\u1f00-\u1fff])+$/.test(val),
|
|
31
|
+
noSpecialCharsEl: (val) => /^([0-9]|[α-ω]|[Α-Ω]|[,]|[.]|[-]|[(]|[)]|[?]|[!]|[;]|[:]|[\n]|[\r]|[ _]|[\u0370-\u03ff\u1f00-\u1fff])+$/.test(val),
|
|
31
32
|
name: (val) => /^[A-Za-zΑ-Ωα-ω\u0370-\u03ff\u1f00-\u1fff\s'-]+$/.test(val),
|
|
32
33
|
tel: (val) => /^(?:\+|00)?[\d\s\-()]{8,20}$/.test(val.replace(/[\s\-()]/g, '')),
|
|
33
34
|
mobile: (val) => /^(?:\+|00)?[\d\s\-()]{8,20}$/.test(val.replace(/[\s\-()]/g, '')),
|