@gov-cy/govcy-express-services 1.4.2 → 1.5.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 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
+ -[CY Login Access Policies](#cy-login-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
  {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gov-cy/govcy-express-services",
3
- "version": "1.4.2",
3
+ "version": "1.5.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",
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, naturalPersonPolicy, handleLoginRoute, handleSigninOidc, handleLogout } from './middleware/cyLoginAuth.mjs';
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, naturalPersonPolicy, (req, res) => {
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
- naturalPersonPolicy, // UNCOMMENT
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
- naturalPersonPolicy, // UNCOMMENT
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
- naturalPersonPolicy,
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
- naturalPersonPolicy, // UNCOMMENT
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyReviewPageHandler(), renderGovcyPage());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(true), govcyPDFRender());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcySuccessPageHandler(), renderGovcyPage());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileViewHandler());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyLoadSubmissionData(), govcyFileDeletePageHandler(), renderGovcyPage());
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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
- naturalPersonPolicy,
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, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyLoadSubmissionData(), govcyPageHandler(), renderGovcyPage());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFileDeletePostHandler());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(), govcyReviewPostHandler());
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, naturalPersonPolicy, govcyServiceEligibilityHandler(true), govcyFormsPostHandler());
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
- /* c8 ignore start */
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
@@ -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.unique_identifier,
500
+ userId: user.sub,
501
501
  redirectUrl
502
502
  });
503
503
  return res.redirect(redirectUrl);
@@ -163,17 +163,17 @@ export const staticResources = {
163
163
  },
164
164
  multipleThingsEnptyState: {
165
165
  en: "You did not add any entries.",
166
- el: "Δεν έχετε προσθέσει ακόμη κάποια καταχώρηση.",
166
+ el: "Δεν έχετε προσθέσει ακόμη κάποια καταχώριση.",
167
167
  tr: "You did not add any entries."
168
168
  },
169
169
  multipleThingsEmptyStateReview: {
170
170
  en: "You did not add any entries.",
171
- el: "Δεν έχετε προσθέσει κάποια καταχώρηση.",
171
+ el: "Δεν έχετε προσθέσει κάποια καταχώριση.",
172
172
  tr: "You did not add any entries yet."
173
173
  },
174
174
  multipleThingsAddEntry: {
175
175
  en: "Add new entry",
176
- el: "Προσθήκη νέας καταχώρησης",
176
+ el: "Προσθήκη νέας καταχώρισης",
177
177
  tr: "Add new entry"
178
178
  },
179
179
  multipleThingsDedupeMessage: {
@@ -193,7 +193,7 @@ export const staticResources = {
193
193
  },
194
194
  multipleThingsItemsValidationPrefix: {
195
195
  en: "Entry {{index}} - ",
196
- el: "Καταχώρηση {{index}} - ",
196
+ el: "Καταχώριση {{index}} - ",
197
197
  tr: "Entry {{index}} - "
198
198
  },
199
199
  multipleThingsAddSuffix: {
@@ -208,12 +208,12 @@ export const staticResources = {
208
208
  },
209
209
  multipleThingsDeleteTitle: {
210
210
  en: "Are you sure you want to delete the item \"{{item}}\"",
211
- el: "Σίγουρα θέλετε να διαγράψετε την καταχώρηση \"{{item}}\"",
211
+ el: "Σίγουρα θέλετε να διαγράψετε την καταχώριση \"{{item}}\"",
212
212
  tr: "Are you sure you want to delete the item \"{{item}}\""
213
213
  },
214
214
  multipleThingsDeleteValidationError: {
215
215
  en: "Select if you want to delete this item",
216
- el: "Επιλέξτε αν θέλετε να διαγράψετε αυτή την καταχώρηση",
216
+ el: "Επιλέξτε αν θέλετε να διαγράψετε αυτή την καταχώριση",
217
217
  tr: "Select if you want to delete the item"
218
218
  },
219
219
  multipleThingsEntries: {
@@ -222,12 +222,12 @@ export const staticResources = {
222
222
  tr: "Entries"
223
223
  },
224
224
  multipleThingsDeleteYesOption: {
225
- el: "Ναι, θέλω να διαγράψω την καταχώρηση",
225
+ el: "Ναι, θέλω να διαγράψω την καταχώριση",
226
226
  en: "Yes, I want to delete this entry",
227
227
  tr: "Yes, I want to delete this entry"
228
228
  },
229
229
  multipleThingsDeleteNoOption: {
230
- el: "Όχι, δεν θέλω να διαγράψω την καταχώρηση",
230
+ el: "Όχι, δεν θέλω να διαγράψω την καταχώριση",
231
231
  en: "No, I don't want to delete this entry",
232
232
  tr: "No, I don't want to delete this entry"
233
233
  },