@gov-cy/govcy-express-services 1.3.0-alpha.2 → 1.3.0-alpha.4
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/package.json +1 -1
- package/src/index.mjs +11 -1
- package/src/middleware/govcyMultipleThingsHubHandler.mjs +0 -1
- package/src/middleware/govcyPageHandler.mjs +11 -4
- package/src/middleware/govcyReviewPostHandler.mjs +48 -6
- package/src/middleware/govcyUpdateMyDetails.mjs +746 -0
- package/src/public/css/govcyExpress.css +4 -0
- package/src/resources/govcyResources.mjs +467 -101
- package/src/utils/govcyConstants.mjs +13 -1
- package/src/utils/govcyDataLayer.mjs +28 -0
- package/src/utils/govcySubmitData.mjs +45 -6
- package/src/utils/govcyValidator.mjs +74 -1
|
@@ -0,0 +1,746 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Update My Details (UMD) page handler
|
|
3
|
+
* Variants:
|
|
4
|
+
* 1️⃣ Manual form — for non-eligible users (no access to UMD)
|
|
5
|
+
* 2️⃣ Confirmation radio — eligible user with existing details
|
|
6
|
+
* 3️⃣ External link — eligible user with no details (redirect to UMD service)
|
|
7
|
+
*
|
|
8
|
+
* GET: Determines variant and builds page template
|
|
9
|
+
* POST: Validates form, stores data (variant 1/2), or redirects to UMD (variant 2/no)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
|
|
13
|
+
import { getEnvVariable, getEnvVariableBool, isProdOrStaging } from "../utils/govcyEnvVariables.mjs";
|
|
14
|
+
import * as govcyResources from "../resources/govcyResources.mjs";
|
|
15
|
+
import * as dataLayer from "../utils/govcyDataLayer.mjs";
|
|
16
|
+
import { logger } from '../utils/govcyLogger.mjs';
|
|
17
|
+
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
|
|
18
|
+
import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
|
|
19
|
+
import { isUnder18, isValidCypriotCitizen, validateFormElements } from "../utils/govcyValidator.mjs";
|
|
20
|
+
import { populateFormData, getFormData } from "../utils/govcyFormHandling.mjs";
|
|
21
|
+
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
|
|
22
|
+
import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs";
|
|
23
|
+
import { UPDATE_MY_DETAILS_ALLOWED_HOSTS } from "../utils/govcyConstants.mjs";
|
|
24
|
+
|
|
25
|
+
export async function govcyUpdateMyDetailsHandler(req, res, next, page, serviceCopy) {
|
|
26
|
+
try {
|
|
27
|
+
const { siteId, pageUrl } = req.params;
|
|
28
|
+
const umdConfig = page?.updateMyDetails;
|
|
29
|
+
|
|
30
|
+
// Sanity checks
|
|
31
|
+
if (
|
|
32
|
+
!umdConfig || // updateMyDetails missing
|
|
33
|
+
!umdConfig.scope || // scope missing
|
|
34
|
+
!Array.isArray(umdConfig.scope) || // scope not an array
|
|
35
|
+
umdConfig.scope.length === 0 || // scope empty
|
|
36
|
+
!umdConfig.APIEndpoint || // APIEndpoint missing
|
|
37
|
+
!umdConfig.APIEndpoint.url || // APIEndpoint.url missing
|
|
38
|
+
!umdConfig.APIEndpoint.clientKey || // clientKey missing
|
|
39
|
+
!umdConfig.APIEndpoint.serviceId || // serviceId missing
|
|
40
|
+
!umdConfig.updateMyDetailsURL // updateMyDetailsURL missing
|
|
41
|
+
) {
|
|
42
|
+
logger.debug("🚨 Invalid updateMyDetails configuration", req);
|
|
43
|
+
return handleMiddlewareError(
|
|
44
|
+
"🚨 Invalid updateMyDetails configuration",
|
|
45
|
+
500,
|
|
46
|
+
next
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
// Environment vars
|
|
50
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
51
|
+
let url = getEnvVariable(umdConfig.APIEndpoint.url || "", false);
|
|
52
|
+
const clientKey = getEnvVariable(umdConfig.APIEndpoint.clientKey || "", false);
|
|
53
|
+
const serviceId = getEnvVariable(umdConfig.APIEndpoint.serviceId || "", false);
|
|
54
|
+
const dsfGtwKey = getEnvVariable(umdConfig?.APIEndpoint?.dsfgtwApiKey || "", "");
|
|
55
|
+
const method = (umdConfig?.APIEndpoint?.method || "GET").toLowerCase();
|
|
56
|
+
const umdBaseURL = getEnvVariable(umdConfig?.updateMyDetailsURL || "", "");
|
|
57
|
+
|
|
58
|
+
// Check if the upload API is configured correctly
|
|
59
|
+
if (!url || !clientKey || !umdBaseURL) {
|
|
60
|
+
return handleMiddlewareError(`Missing environment variables for updateMyDetails`, 500, next);
|
|
61
|
+
}
|
|
62
|
+
// Build hub template
|
|
63
|
+
let pageTemplate = {};
|
|
64
|
+
|
|
65
|
+
// Get the user
|
|
66
|
+
const user = dataLayer.getUser(req.session);
|
|
67
|
+
|
|
68
|
+
let pageVariant = 0;
|
|
69
|
+
|
|
70
|
+
// Check if the user is a cypriot
|
|
71
|
+
if (!isValidCypriotCitizen(user)) {
|
|
72
|
+
// --------------- Not eligible for Update my details ---------------
|
|
73
|
+
// --------------- Page variant 1
|
|
74
|
+
pageVariant = 1;
|
|
75
|
+
// load the manual input page
|
|
76
|
+
pageTemplate = createUmdManualPageTemplate(siteId, serviceCopy.site.lang, page, req);
|
|
77
|
+
} else {
|
|
78
|
+
// --------------- Eligible for Update my details ---------------
|
|
79
|
+
// Construct the URL with the language
|
|
80
|
+
url += `/${serviceCopy.site.lang}`;
|
|
81
|
+
|
|
82
|
+
// run the API request to check if the user has already uploaded their details
|
|
83
|
+
// Perform the upload request
|
|
84
|
+
const response = await govcyApiRequest(
|
|
85
|
+
method,
|
|
86
|
+
url,
|
|
87
|
+
{},
|
|
88
|
+
true,
|
|
89
|
+
user,
|
|
90
|
+
{
|
|
91
|
+
accept: "text/plain",
|
|
92
|
+
"client-key": clientKey,
|
|
93
|
+
"service-id": serviceId,
|
|
94
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
|
|
95
|
+
},
|
|
96
|
+
3,
|
|
97
|
+
allowSelfSignedCerts
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// If not succeeded, handle error
|
|
101
|
+
if (!response?.Succeeded) {
|
|
102
|
+
return handleMiddlewareError(`updateMyDetailsAPIEndpoint - returned succeeded false`, 500, next);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check if the response contains the expected data
|
|
106
|
+
if (!response?.Data || !response?.Data?.dob) {
|
|
107
|
+
return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data`, 500, next);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// calculate if person in under 18 based on date of birth
|
|
111
|
+
if (isUnder18(response.Data.dob)) {
|
|
112
|
+
// --------------- Not eligible for Update my details ---------------
|
|
113
|
+
// --------------- Page variant 1
|
|
114
|
+
pageVariant = 1;
|
|
115
|
+
// load the manual input page
|
|
116
|
+
pageTemplate = createUmdManualPageTemplate(siteId, serviceCopy.site.lang, page, req);
|
|
117
|
+
} else {
|
|
118
|
+
let hasData = true;
|
|
119
|
+
let userDetails = {};
|
|
120
|
+
//for each element in the scope array
|
|
121
|
+
for (const element of umdConfig?.scope || []) {
|
|
122
|
+
// The key in
|
|
123
|
+
let key = element;
|
|
124
|
+
|
|
125
|
+
// Get the value
|
|
126
|
+
let value = response.Data?.[key] || "";
|
|
127
|
+
|
|
128
|
+
// Special case for address
|
|
129
|
+
if (element === "address") {
|
|
130
|
+
key = "addressInfo";
|
|
131
|
+
value = response.Data?.addressInfo?.[0]?.addressText || "";
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check if the key exists
|
|
135
|
+
if (!Object.prototype.hasOwnProperty.call(response.Data || {}, key)) {
|
|
136
|
+
hasData = false;
|
|
137
|
+
return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data for element ${element}`, 500, next);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Check if the value is null, undefined, or empty string
|
|
141
|
+
if (value == null || value === "") {
|
|
142
|
+
// Set hasData to false and set the value to an empty string
|
|
143
|
+
hasData = false;
|
|
144
|
+
userDetails[element] = "";
|
|
145
|
+
} else {
|
|
146
|
+
// Set the value
|
|
147
|
+
userDetails[element] = value;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (hasData) {
|
|
152
|
+
// --------------- Page variant 2: Confirmation radio for eligible users with data
|
|
153
|
+
pageVariant = 2;
|
|
154
|
+
// load the has data page
|
|
155
|
+
pageTemplate = createUmdHasDataPageTemplate(siteId, serviceCopy.site.lang, page, req, userDetails);
|
|
156
|
+
} else {
|
|
157
|
+
// --------------- Page variant 3: External redirect link for users with no data
|
|
158
|
+
pageVariant = 3;
|
|
159
|
+
// load the has no data page
|
|
160
|
+
pageTemplate = createUmdHasNoDataPageTemplate(siteId, serviceCopy.site.lang, page, req, umdBaseURL);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// if the page variant is 1 or 2 which means it has a form
|
|
166
|
+
if (pageVariant === 1 || pageVariant === 2) {
|
|
167
|
+
// Handle form data
|
|
168
|
+
let theData = {};
|
|
169
|
+
|
|
170
|
+
//--------- Handle Validation Errors ---------
|
|
171
|
+
// Check if validation errors exist in the session
|
|
172
|
+
const validationErrors = dataLayer.getPageValidationErrors(req.session, siteId, pageUrl);
|
|
173
|
+
if (validationErrors) {
|
|
174
|
+
// Populate form data from validation errors
|
|
175
|
+
theData = validationErrors?.formData || {};
|
|
176
|
+
} else {
|
|
177
|
+
// Populate form data from session
|
|
178
|
+
theData = dataLayer.getPageData(req.session, siteId, pageUrl);
|
|
179
|
+
}
|
|
180
|
+
//--------- End of Handle Validation Errors ---------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
populateFormData(
|
|
184
|
+
pageTemplate.sections[0].elements[0].params.elements,
|
|
185
|
+
theData,
|
|
186
|
+
validationErrors,
|
|
187
|
+
req.session,
|
|
188
|
+
siteId,
|
|
189
|
+
pageUrl,
|
|
190
|
+
req.globalLang,
|
|
191
|
+
null,
|
|
192
|
+
req.query.route);
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
// if there are validation errors, add an error summary
|
|
196
|
+
if (validationErrors?.errorSummary?.length > 0) {
|
|
197
|
+
pageTemplate.sections[0].elements[0].params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Add topElements if provided
|
|
202
|
+
if (Array.isArray(umdConfig.topElements)) {
|
|
203
|
+
pageTemplate.sections[0].elements[0].params.elements.unshift(...umdConfig.topElements);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
//if hasBackLink == true add section beforeMain with backlink element
|
|
207
|
+
if (umdConfig?.hasBackLink == true) {
|
|
208
|
+
pageTemplate.sections.unshift({
|
|
209
|
+
name: "beforeMain",
|
|
210
|
+
elements: [
|
|
211
|
+
{
|
|
212
|
+
element: "backLink",
|
|
213
|
+
params: {}
|
|
214
|
+
}
|
|
215
|
+
]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Attach processed data to request
|
|
220
|
+
req.processedPage = {
|
|
221
|
+
pageData: {
|
|
222
|
+
site: serviceCopy.site,
|
|
223
|
+
pageData: {
|
|
224
|
+
title: govcyResources.staticResources.text.updateMyDetailsTitle,
|
|
225
|
+
layout: page?.pageData?.layout || "layouts/govcyBase.njk",
|
|
226
|
+
mainLayout: page?.pageData?.mainLayout || "two-third"
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
pageTemplate: pageTemplate
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
logger.debug("Processed `govcyUpdateMyDetailsHandler` page data:", req.processedPage, req);
|
|
233
|
+
next(); // Pass control to the next middleware or route
|
|
234
|
+
|
|
235
|
+
} catch (error) {
|
|
236
|
+
logger.debug("Error in govcyUpdateMyDetailsHandler middleware:", error.message);
|
|
237
|
+
return next(error); // Pass the error to the next middleware
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Middleware to handle page form submission for updateMyDetails
|
|
244
|
+
*/
|
|
245
|
+
export function govcyUpdateMyDetailsPostHandler() {
|
|
246
|
+
return async (req, res, next) => {
|
|
247
|
+
try {
|
|
248
|
+
const { siteId, pageUrl } = req.params;
|
|
249
|
+
|
|
250
|
+
// ⤵️ Load service and check if it exists
|
|
251
|
+
const service = req.serviceData;
|
|
252
|
+
|
|
253
|
+
// ⤵️ Find the current page based on the URL
|
|
254
|
+
const page = getPageConfigData(service, pageUrl);
|
|
255
|
+
|
|
256
|
+
if (!service || !page) {
|
|
257
|
+
return handleMiddlewareError("Service or page data missing", 400, next);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ----- Conditional logic comes here
|
|
261
|
+
// ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away)
|
|
262
|
+
const conditionResult = evaluatePageConditions(page, req.session, siteId, req);
|
|
263
|
+
if (conditionResult.result === false) {
|
|
264
|
+
logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult);
|
|
265
|
+
return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect));
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
//-----------------------------------------------------------------------------
|
|
269
|
+
// UpdateMyDetails configuration
|
|
270
|
+
const umdConfig = page?.updateMyDetails;
|
|
271
|
+
|
|
272
|
+
// Sanity checks
|
|
273
|
+
if (
|
|
274
|
+
!umdConfig || // updateMyDetails missing
|
|
275
|
+
!umdConfig.scope || // scope missing
|
|
276
|
+
!Array.isArray(umdConfig.scope) || // scope not an array
|
|
277
|
+
umdConfig.scope.length === 0 || // scope empty
|
|
278
|
+
!umdConfig.APIEndpoint || // APIEndpoint missing
|
|
279
|
+
!umdConfig.APIEndpoint.url || // APIEndpoint.url missing
|
|
280
|
+
!umdConfig.APIEndpoint.clientKey || // clientKey missing
|
|
281
|
+
!umdConfig.APIEndpoint.serviceId || // serviceId missing
|
|
282
|
+
!umdConfig.updateMyDetailsURL // updateMyDetailsURL missing
|
|
283
|
+
) {
|
|
284
|
+
logger.debug("🚨 Invalid updateMyDetails configuration", req);
|
|
285
|
+
return handleMiddlewareError(
|
|
286
|
+
"🚨 Invalid updateMyDetails configuration",
|
|
287
|
+
500,
|
|
288
|
+
next
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
// Environment vars
|
|
292
|
+
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
|
|
293
|
+
let url = getEnvVariable(umdConfig.APIEndpoint.url || "", false);
|
|
294
|
+
const clientKey = getEnvVariable(umdConfig.APIEndpoint.clientKey || "", false);
|
|
295
|
+
const serviceId = getEnvVariable(umdConfig.APIEndpoint.serviceId || "", false);
|
|
296
|
+
const dsfGtwKey = getEnvVariable(umdConfig?.APIEndpoint?.dsfgtwApiKey || "", "");
|
|
297
|
+
const method = (umdConfig?.APIEndpoint?.method || "GET").toLowerCase();
|
|
298
|
+
const umdBaseURL = getEnvVariable(umdConfig?.updateMyDetailsURL || "", "");
|
|
299
|
+
|
|
300
|
+
// Check if the upload API is configured correctly
|
|
301
|
+
if (!url || !clientKey || !umdBaseURL) {
|
|
302
|
+
return handleMiddlewareError(`Missing environment variables for updateMyDetails`, 500, next);
|
|
303
|
+
}
|
|
304
|
+
// Build hub template
|
|
305
|
+
let pageTemplate = {};
|
|
306
|
+
|
|
307
|
+
// user details (for variant 2: Confirmation radio for eligible users with data)
|
|
308
|
+
let userDetails = {};
|
|
309
|
+
|
|
310
|
+
// Get the user
|
|
311
|
+
const user = dataLayer.getUser(req.session);
|
|
312
|
+
|
|
313
|
+
let pageVariant = 0;
|
|
314
|
+
|
|
315
|
+
// Check if the user is a cypriot
|
|
316
|
+
if (!isValidCypriotCitizen(user)) {
|
|
317
|
+
// --------------- Not eligible for Update my details ---------------
|
|
318
|
+
// --------------- Page variant 1:Manual form for non-eligible users
|
|
319
|
+
pageVariant = 1;
|
|
320
|
+
// load the manual input page
|
|
321
|
+
pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
|
|
322
|
+
} else {
|
|
323
|
+
// --------------- Eligible for Update my details ---------------
|
|
324
|
+
// Construct the URL with the language
|
|
325
|
+
url += `/${service.site.lang}`;
|
|
326
|
+
|
|
327
|
+
// run the API request to check if the user has already uploaded their details
|
|
328
|
+
// Perform the upload request
|
|
329
|
+
const response = await govcyApiRequest(
|
|
330
|
+
method,
|
|
331
|
+
url,
|
|
332
|
+
{},
|
|
333
|
+
true,
|
|
334
|
+
user,
|
|
335
|
+
{
|
|
336
|
+
accept: "text/plain",
|
|
337
|
+
"client-key": clientKey,
|
|
338
|
+
"service-id": serviceId,
|
|
339
|
+
...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey })
|
|
340
|
+
},
|
|
341
|
+
3,
|
|
342
|
+
allowSelfSignedCerts
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// If not succeeded, handle error
|
|
346
|
+
if (!response?.Succeeded) {
|
|
347
|
+
return handleMiddlewareError(`updateMyDetailsAPIEndpoint - returned succeeded false`, 500, next);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Check if the response contains the expected data
|
|
351
|
+
if (!response?.Data || !response?.Data?.dob) {
|
|
352
|
+
return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data`, 500, next);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// calculate if person in under 18 based on date of birth
|
|
356
|
+
if (isUnder18(response.Data.dob)) {
|
|
357
|
+
// --------------- Not eligible for Update my details ---------------
|
|
358
|
+
// --------------- Page variant 1:Manual form for non-eligible users
|
|
359
|
+
pageVariant = 1;
|
|
360
|
+
// load the manual input page
|
|
361
|
+
pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req);
|
|
362
|
+
} else {
|
|
363
|
+
let hasData = true;
|
|
364
|
+
//for each element in the scope array
|
|
365
|
+
for (const element of umdConfig?.scope || []) {
|
|
366
|
+
// The key in
|
|
367
|
+
let key = element;
|
|
368
|
+
|
|
369
|
+
// Get the value
|
|
370
|
+
let value = response.Data?.[key] || "";
|
|
371
|
+
|
|
372
|
+
// Special case for address
|
|
373
|
+
if (element === "address") {
|
|
374
|
+
key = "addressInfo";
|
|
375
|
+
value = response.Data?.addressInfo?.[0]?.addressText || "";
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Check if the key exists
|
|
379
|
+
if (!Object.prototype.hasOwnProperty.call(response.Data || {}, key)) {
|
|
380
|
+
hasData = false;
|
|
381
|
+
return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data for element ${element}`, 500, next);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Check if the value is null, undefined, or empty string
|
|
385
|
+
if (value == null || value === "") {
|
|
386
|
+
// Set hasData to false and set the value to an empty string
|
|
387
|
+
hasData = false;
|
|
388
|
+
userDetails[element] = "";
|
|
389
|
+
} else {
|
|
390
|
+
// Set the value
|
|
391
|
+
userDetails[element] = value;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (hasData) {
|
|
396
|
+
// --------------- Page variant 2: Confirmation radio for eligible users with data
|
|
397
|
+
pageVariant = 2;
|
|
398
|
+
// load the has data page
|
|
399
|
+
pageTemplate = createUmdHasDataPageTemplate(siteId, service.site.lang, page, req, userDetails);
|
|
400
|
+
} else {
|
|
401
|
+
// --------------- Page variant 3: External redirect link for users with no data
|
|
402
|
+
return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Unexpected POST for User that has no Update my details data.`, 400, next);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
//-----------------------------------------------------------------------------
|
|
410
|
+
// 🔍 Find the form definition inside `pageTemplate.sections`
|
|
411
|
+
let formElement = null;
|
|
412
|
+
for (const section of pageTemplate.sections) {
|
|
413
|
+
formElement = section.elements.find(el => el.element === "form");
|
|
414
|
+
if (formElement) break;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (!formElement) {
|
|
418
|
+
return handleMiddlewareError("🚨 Form definition not found.", 500, next);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let nextPage = null;
|
|
422
|
+
|
|
423
|
+
// const formData = req.body; // Submitted data
|
|
424
|
+
const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data
|
|
425
|
+
|
|
426
|
+
// ☑️ Start validation from top-level form elements
|
|
427
|
+
const validationErrors = validateFormElements(formElement.params.elements, formData);
|
|
428
|
+
|
|
429
|
+
// ❌ Return validation errors if any exist
|
|
430
|
+
if (Object.keys(validationErrors).length > 0) {
|
|
431
|
+
logger.debug("🚨 Validation errors:", validationErrors, req);
|
|
432
|
+
logger.info("🚨 Validation errors on:", req.originalUrl);
|
|
433
|
+
// store the validation errors
|
|
434
|
+
dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData);
|
|
435
|
+
//redirect to the same page with error summary
|
|
436
|
+
return res.redirect(govcyResources.constructErrorSummaryUrl(
|
|
437
|
+
govcyResources.constructPageUrl(siteId, page.pageData.url, (req.query.route === "review" ? "review" : ""))
|
|
438
|
+
));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (pageVariant === 1) {
|
|
442
|
+
// --------------- Page variant 1:Manual form for non-eligible users
|
|
443
|
+
//⤴️ Store validated form data in session
|
|
444
|
+
dataLayer.storePageData(req.session, siteId, pageUrl, formData);
|
|
445
|
+
dataLayer.storePageUpdateMyDetails(req.session, siteId, pageUrl, formData);
|
|
446
|
+
} else if (pageVariant === 2) {
|
|
447
|
+
// --------------- Page variant 2: Confirmation radio for eligible users with data
|
|
448
|
+
const userChoice = req.body?.useTheseDetails?.trim().toLowerCase();
|
|
449
|
+
if (userChoice === "yes") {
|
|
450
|
+
//⤴️ Store validated form data in session
|
|
451
|
+
dataLayer.storePageData(req.session, siteId, pageUrl, userDetails);
|
|
452
|
+
dataLayer.storePageUpdateMyDetails(req.session, siteId, pageUrl, userDetails);
|
|
453
|
+
} else if (userChoice === "no") {
|
|
454
|
+
// construct the return url to go to `:siteId/:pageUrl` + `?route=` + `review`
|
|
455
|
+
const returnUrl = `${req.protocol}://${req.get("host")}${govcyResources.constructPageUrl(siteId, page.pageData.url, (req.query.route === "review" ? "review" : ""))}`;
|
|
456
|
+
// Get user profile id
|
|
457
|
+
const userId = user?.sub || "";
|
|
458
|
+
// 🔄 User chose to update their details externally
|
|
459
|
+
const redirectUrl = constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, returnUrl);
|
|
460
|
+
logger.info("User opted to update details externally", {
|
|
461
|
+
userId: user.unique_identifier,
|
|
462
|
+
redirectUrl
|
|
463
|
+
});
|
|
464
|
+
return res.redirect(redirectUrl);
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
// 🚨 Should never happen (defensive)
|
|
468
|
+
return handleMiddlewareError("Invalid value for useTheseDetails", 400, next);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
// 🔄 Fire-and-forget temporary save (non-blocking)
|
|
474
|
+
(async () => {
|
|
475
|
+
try { await tempSaveIfConfigured(req.session, service, siteId); }
|
|
476
|
+
catch (e) { /* already logged internally */ }
|
|
477
|
+
})();
|
|
478
|
+
|
|
479
|
+
logger.debug("✅ Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req);
|
|
480
|
+
logger.info("✅ Form submitted successfully:", req.originalUrl);
|
|
481
|
+
|
|
482
|
+
// 🔍 Determine next page (if applicable)
|
|
483
|
+
for (const section of pageTemplate.sections) {
|
|
484
|
+
const form = section.elements.find(el => el.element === "form");
|
|
485
|
+
if (form) {
|
|
486
|
+
//handle review route
|
|
487
|
+
if (req.query.route === "review") {
|
|
488
|
+
nextPage = govcyResources.constructPageUrl(siteId, "review");
|
|
489
|
+
} else {
|
|
490
|
+
nextPage = page.pageData.nextPage;
|
|
491
|
+
//nextPage = form.params.elements.find(el => el.element === "button" && el.params?.prototypeNavigate)?.params.prototypeNavigate;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// ➡️ Redirect to the next page if defined, otherwise return success
|
|
497
|
+
if (nextPage) {
|
|
498
|
+
logger.debug("🔄 Redirecting to next page:", nextPage, req);
|
|
499
|
+
// 🛠 Fix relative paths
|
|
500
|
+
return res.redirect(govcyResources.constructPageUrl(siteId, `${nextPage.split('/').pop()}`));
|
|
501
|
+
}
|
|
502
|
+
res.json({ success: true, message: "Form submitted successfully" });
|
|
503
|
+
|
|
504
|
+
} catch (error) {
|
|
505
|
+
return next(error); // Pass error to govcyHttpErrorHandler
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Creates the has data page template for users that have data in Update My Details
|
|
513
|
+
* @param {string} siteId The site id
|
|
514
|
+
* @param {string} lang The language
|
|
515
|
+
* @param {object} page The page object
|
|
516
|
+
* @param {object} req The request object
|
|
517
|
+
* @param {Array} userDetails The user details
|
|
518
|
+
* @returns {object} The page template
|
|
519
|
+
*/
|
|
520
|
+
function createUmdHasDataPageTemplate(siteId, lang, page, req, userDetails) {
|
|
521
|
+
const umdConfig = page?.updateMyDetails || {};
|
|
522
|
+
// Build hub template
|
|
523
|
+
const pageTemplate = {
|
|
524
|
+
sections: [
|
|
525
|
+
{
|
|
526
|
+
name: "main",
|
|
527
|
+
elements: [
|
|
528
|
+
{
|
|
529
|
+
element: "form",
|
|
530
|
+
params: {
|
|
531
|
+
action: govcyResources.constructPageUrl(siteId, `${page.pageData.url}/update-my-details-response`, (req.query.route === "review" ? "review" : "")),
|
|
532
|
+
method: "POST",
|
|
533
|
+
elements: [govcyResources.csrfTokenInput(req.csrfToken())]
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
]
|
|
537
|
+
}
|
|
538
|
+
]
|
|
539
|
+
};
|
|
540
|
+
|
|
541
|
+
// the continue button
|
|
542
|
+
let continueButton = {
|
|
543
|
+
element: "button",
|
|
544
|
+
params: {
|
|
545
|
+
// if no continue button is provided use the static resource
|
|
546
|
+
// text: (
|
|
547
|
+
// umdConfig?.continueButtonText?.[lang]
|
|
548
|
+
// ? umdConfig.continueButtonText
|
|
549
|
+
// : govcyResources.staticResources.text.continue
|
|
550
|
+
// ),
|
|
551
|
+
text: govcyResources.staticResources.text.continue,
|
|
552
|
+
variant: "primary",
|
|
553
|
+
type: "submit"
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// ➕ Add header and instructions
|
|
558
|
+
pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["header"]);
|
|
559
|
+
pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["instructions"]);
|
|
560
|
+
|
|
561
|
+
// ➕ The data summaryList
|
|
562
|
+
let summaryList = {
|
|
563
|
+
element: "summaryList",
|
|
564
|
+
params: {
|
|
565
|
+
items: []
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
//for each element in the scope array
|
|
569
|
+
umdConfig?.scope.forEach(element => {
|
|
570
|
+
// add the key and value to the summaryList
|
|
571
|
+
summaryList.params.items.push({
|
|
572
|
+
key: govcyResources.staticResources.text.updateMyDetailsScopes[element],
|
|
573
|
+
value: [
|
|
574
|
+
{
|
|
575
|
+
element: "textElement",
|
|
576
|
+
params: {
|
|
577
|
+
type: "span",
|
|
578
|
+
classes: "govcy-whitespace-pre-line",
|
|
579
|
+
text: govcyResources.getSameMultilingualObject(null, userDetails[element])
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
})
|
|
584
|
+
})
|
|
585
|
+
// ➕ Add the element
|
|
586
|
+
pageTemplate.sections[0].elements[0].params.elements.push(summaryList);
|
|
587
|
+
|
|
588
|
+
// ➕ Add the question
|
|
589
|
+
pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["question"]);
|
|
590
|
+
|
|
591
|
+
// ➕ Add the continue button
|
|
592
|
+
pageTemplate.sections[0].elements[0].params.elements.push(continueButton);
|
|
593
|
+
|
|
594
|
+
return pageTemplate;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Creates the has no data page template for users that have no data in Update My Details
|
|
599
|
+
* @param {string} siteId The site id
|
|
600
|
+
* @param {string} lang The language
|
|
601
|
+
* @param {object} page The page object
|
|
602
|
+
* @param {object} req The request object
|
|
603
|
+
* @returns {object} The page template
|
|
604
|
+
*/
|
|
605
|
+
function createUmdHasNoDataPageTemplate(siteId, lang, page, req, umdBaseURL) {
|
|
606
|
+
const umdConfig = page?.updateMyDetails || {};
|
|
607
|
+
|
|
608
|
+
// Get user
|
|
609
|
+
const user = dataLayer.getUser(req.session);
|
|
610
|
+
// Get user profile id
|
|
611
|
+
const userId = user?.sub || "";
|
|
612
|
+
const redirectUrl = constructUpdateMyDetailsRedirect(req, userId, umdBaseURL)
|
|
613
|
+
// deep copy the continue button
|
|
614
|
+
const continueButtonText = JSON.parse(JSON.stringify(govcyResources.staticResources.text.continue))
|
|
615
|
+
|
|
616
|
+
// Replace label placeholders on page title
|
|
617
|
+
for (const lang of Object.keys(continueButtonText)) {
|
|
618
|
+
continueButtonText[lang] = `<a class="govcy-btn-primary" href="${redirectUrl}">${continueButtonText[lang]}</a>`;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Build hub template
|
|
622
|
+
const pageTemplate = {
|
|
623
|
+
sections: [
|
|
624
|
+
{
|
|
625
|
+
name: "main",
|
|
626
|
+
elements: [
|
|
627
|
+
{
|
|
628
|
+
element: "form",
|
|
629
|
+
params: {
|
|
630
|
+
elements: [
|
|
631
|
+
govcyResources.staticResources.elements.umdHasNoData.header,
|
|
632
|
+
govcyResources.staticResources.elements.umdHasNoData.instructions,
|
|
633
|
+
{
|
|
634
|
+
element: "htmlElement",
|
|
635
|
+
params: {
|
|
636
|
+
text: continueButtonText
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
]
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
]
|
|
643
|
+
}
|
|
644
|
+
]
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
return pageTemplate;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Creates the page template for the updateMyDetails manual input page
|
|
652
|
+
* @param {string} siteId The site id
|
|
653
|
+
* @param {string} lang The language
|
|
654
|
+
* @param {object} page The page object
|
|
655
|
+
* @param {object} req The request object
|
|
656
|
+
* @returns {object} The page template
|
|
657
|
+
*/
|
|
658
|
+
export function createUmdManualPageTemplate(siteId, lang, page, req) {
|
|
659
|
+
const umdConfig = page?.updateMyDetails || {};
|
|
660
|
+
// Build hub template
|
|
661
|
+
const pageTemplate = {
|
|
662
|
+
sections: [
|
|
663
|
+
{
|
|
664
|
+
name: "main",
|
|
665
|
+
elements: [
|
|
666
|
+
{
|
|
667
|
+
element: "form",
|
|
668
|
+
params: {
|
|
669
|
+
action: govcyResources.constructPageUrl(siteId, `${page.pageData.url}/update-my-details-response`, (req.query.route === "review" ? "review" : "")),
|
|
670
|
+
method: "POST",
|
|
671
|
+
elements: [govcyResources.csrfTokenInput(req.csrfToken())]
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
]
|
|
675
|
+
}
|
|
676
|
+
]
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
// the continue button
|
|
681
|
+
let continueButton = {
|
|
682
|
+
element: "button",
|
|
683
|
+
params: {
|
|
684
|
+
// text: (
|
|
685
|
+
// // if no continue button is provided use the static resource
|
|
686
|
+
// umdConfig?.continueButtonText?.[lang]
|
|
687
|
+
// ? umdConfig.continueButtonText
|
|
688
|
+
// : govcyResources.staticResources.text.continue
|
|
689
|
+
// ),
|
|
690
|
+
text: govcyResources.staticResources.text.continue,
|
|
691
|
+
variant: "primary",
|
|
692
|
+
type: "submit"
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ➕ Add header and instructions
|
|
697
|
+
pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual["header"]);
|
|
698
|
+
pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual["instructions"]);
|
|
699
|
+
//for each element in the scope array
|
|
700
|
+
umdConfig?.scope.forEach(element => {
|
|
701
|
+
// ➕ Add the element
|
|
702
|
+
pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual[element]);
|
|
703
|
+
|
|
704
|
+
})
|
|
705
|
+
// ➕ Add the continue button
|
|
706
|
+
pageTemplate.sections[0].elements[0].params.elements.push(continueButton);
|
|
707
|
+
|
|
708
|
+
return pageTemplate;
|
|
709
|
+
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/**
|
|
713
|
+
* Constructs the redirect URL for Update My Details
|
|
714
|
+
* @param {object} req The request object
|
|
715
|
+
* @param {string} userId The user id
|
|
716
|
+
* @param {string} umdBaseURL The Update My Details base URL
|
|
717
|
+
* @param {string} returnUrl (Optional) The return URL
|
|
718
|
+
* @returns {string} The redirect URL
|
|
719
|
+
*/
|
|
720
|
+
export function constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, returnUrl = "") {
|
|
721
|
+
// Only allow certain hosts
|
|
722
|
+
const allowedHosts = UPDATE_MY_DETAILS_ALLOWED_HOSTS;
|
|
723
|
+
|
|
724
|
+
// Validate URL against allowed hosts
|
|
725
|
+
const parsed = new URL(umdBaseURL);
|
|
726
|
+
if (!allowedHosts.includes(parsed.hostname)) {
|
|
727
|
+
throw new Error("Invalid Update My Details URL");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (returnUrl === "") {
|
|
731
|
+
// Construct return URL
|
|
732
|
+
returnUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// Validate return URL for production only (HTTPS)
|
|
736
|
+
if (!returnUrl.startsWith("https://") && isProdOrStaging() === "production") {
|
|
737
|
+
throw new Error("Return URL must be HTTPS in production");
|
|
738
|
+
}
|
|
739
|
+
// Encode url args
|
|
740
|
+
const encodedReturnUrl = encodeURIComponent(Buffer.from(returnUrl).toString("base64"));
|
|
741
|
+
const encodedUserId = encodeURIComponent(Buffer.from(userId).toString("base64"));
|
|
742
|
+
const lang = req.globalLang || "el";
|
|
743
|
+
|
|
744
|
+
// Construct redirect URL
|
|
745
|
+
return `${umdBaseURL}/ReturnUrl/SetReturnUrl?Url=${encodedReturnUrl}&UserProfileId=${encodedUserId}&lang=${lang}`;
|
|
746
|
+
}
|