@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.
@@ -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
+ }