@gov-cy/govcy-express-services 1.3.0-alpha.2 → 1.3.0-alpha.3

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,744 @@
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
+ } else if (pageVariant === 2) {
446
+ // --------------- Page variant 2: Confirmation radio for eligible users with data
447
+ const userChoice = req.body?.useTheseDetails?.trim().toLowerCase();
448
+ if (userChoice === "yes") {
449
+ //⤴️ Store validated form data in session
450
+ dataLayer.storePageData(req.session, siteId, pageUrl, userDetails);
451
+ } else if (userChoice === "no") {
452
+ // construct the return url to go to `:siteId/:pageUrl` + `?route=` + `review`
453
+ const returnUrl = `${req.protocol}://${req.get("host")}${govcyResources.constructPageUrl(siteId, page.pageData.url, (req.query.route === "review" ? "review" : ""))}`;
454
+ // Get user profile id
455
+ const userId = user?.sub || "";
456
+ // 🔄 User chose to update their details externally
457
+ const redirectUrl = constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, returnUrl);
458
+ logger.info("User opted to update details externally", {
459
+ userId: user.unique_identifier,
460
+ redirectUrl
461
+ });
462
+ return res.redirect(redirectUrl);
463
+ }
464
+ else {
465
+ // 🚨 Should never happen (defensive)
466
+ return handleMiddlewareError("Invalid value for useTheseDetails", 400, next);
467
+ }
468
+ }
469
+
470
+
471
+ // 🔄 Fire-and-forget temporary save (non-blocking)
472
+ (async () => {
473
+ try { await tempSaveIfConfigured(req.session, service, siteId); }
474
+ catch (e) { /* already logged internally */ }
475
+ })();
476
+
477
+ logger.debug("✅ Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req);
478
+ logger.info("✅ Form submitted successfully:", req.originalUrl);
479
+
480
+ // 🔍 Determine next page (if applicable)
481
+ for (const section of pageTemplate.sections) {
482
+ const form = section.elements.find(el => el.element === "form");
483
+ if (form) {
484
+ //handle review route
485
+ if (req.query.route === "review") {
486
+ nextPage = govcyResources.constructPageUrl(siteId, "review");
487
+ } else {
488
+ nextPage = page.pageData.nextPage;
489
+ //nextPage = form.params.elements.find(el => el.element === "button" && el.params?.prototypeNavigate)?.params.prototypeNavigate;
490
+ }
491
+ }
492
+ }
493
+
494
+ // ➡️ Redirect to the next page if defined, otherwise return success
495
+ if (nextPage) {
496
+ logger.debug("🔄 Redirecting to next page:", nextPage, req);
497
+ // 🛠 Fix relative paths
498
+ return res.redirect(govcyResources.constructPageUrl(siteId, `${nextPage.split('/').pop()}`));
499
+ }
500
+ res.json({ success: true, message: "Form submitted successfully" });
501
+
502
+ } catch (error) {
503
+ return next(error); // Pass error to govcyHttpErrorHandler
504
+ }
505
+ };
506
+ }
507
+
508
+
509
+ /**
510
+ * Creates the has data page template for users that have data in Update My Details
511
+ * @param {string} siteId The site id
512
+ * @param {string} lang The language
513
+ * @param {object} page The page object
514
+ * @param {object} req The request object
515
+ * @param {Array} userDetails The user details
516
+ * @returns {object} The page template
517
+ */
518
+ function createUmdHasDataPageTemplate(siteId, lang, page, req, userDetails) {
519
+ const umdConfig = page?.updateMyDetails || {};
520
+ // Build hub template
521
+ const pageTemplate = {
522
+ sections: [
523
+ {
524
+ name: "main",
525
+ elements: [
526
+ {
527
+ element: "form",
528
+ params: {
529
+ action: govcyResources.constructPageUrl(siteId, `${page.pageData.url}/update-my-details-response`, (req.query.route === "review" ? "review" : "")),
530
+ method: "POST",
531
+ elements: [govcyResources.csrfTokenInput(req.csrfToken())]
532
+ }
533
+ }
534
+ ]
535
+ }
536
+ ]
537
+ };
538
+
539
+ // the continue button
540
+ let continueButton = {
541
+ element: "button",
542
+ params: {
543
+ // if no continue button is provided use the static resource
544
+ // text: (
545
+ // umdConfig?.continueButtonText?.[lang]
546
+ // ? umdConfig.continueButtonText
547
+ // : govcyResources.staticResources.text.continue
548
+ // ),
549
+ text: govcyResources.staticResources.text.continue,
550
+ variant: "primary",
551
+ type: "submit"
552
+ }
553
+ }
554
+
555
+ // ➕ Add header and instructions
556
+ pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["header"]);
557
+ pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["instructions"]);
558
+
559
+ // ➕ The data summaryList
560
+ let summaryList = {
561
+ element: "summaryList",
562
+ params: {
563
+ items: []
564
+ }
565
+ }
566
+ //for each element in the scope array
567
+ umdConfig?.scope.forEach(element => {
568
+ // add the key and value to the summaryList
569
+ summaryList.params.items.push({
570
+ key: govcyResources.staticResources.text.updateMyDetailsScopes[element],
571
+ value: [
572
+ {
573
+ element: "textElement",
574
+ params: {
575
+ type: "span",
576
+ classes: "govcy-whitespace-pre-line",
577
+ text: govcyResources.getSameMultilingualObject(null, userDetails[element])
578
+ }
579
+ }
580
+ ]
581
+ })
582
+ })
583
+ // ➕ Add the element
584
+ pageTemplate.sections[0].elements[0].params.elements.push(summaryList);
585
+
586
+ // ➕ Add the question
587
+ pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["question"]);
588
+
589
+ // ➕ Add the continue button
590
+ pageTemplate.sections[0].elements[0].params.elements.push(continueButton);
591
+
592
+ return pageTemplate;
593
+ }
594
+
595
+ /**
596
+ * Creates the has no data page template for users that have no data in Update My Details
597
+ * @param {string} siteId The site id
598
+ * @param {string} lang The language
599
+ * @param {object} page The page object
600
+ * @param {object} req The request object
601
+ * @returns {object} The page template
602
+ */
603
+ function createUmdHasNoDataPageTemplate(siteId, lang, page, req, umdBaseURL) {
604
+ const umdConfig = page?.updateMyDetails || {};
605
+
606
+ // Get user
607
+ const user = dataLayer.getUser(req.session);
608
+ // Get user profile id
609
+ const userId = user?.sub || "";
610
+ const redirectUrl = constructUpdateMyDetailsRedirect(req, userId, umdBaseURL)
611
+ // deep copy the continue button
612
+ const continueButtonText = JSON.parse(JSON.stringify(govcyResources.staticResources.text.continue))
613
+
614
+ // Replace label placeholders on page title
615
+ for (const lang of Object.keys(continueButtonText)) {
616
+ continueButtonText[lang] = `<a class="govcy-btn-primary" href="${redirectUrl}">${continueButtonText[lang]}</a>`;
617
+ }
618
+
619
+ // Build hub template
620
+ const pageTemplate = {
621
+ sections: [
622
+ {
623
+ name: "main",
624
+ elements: [
625
+ {
626
+ element: "form",
627
+ params: {
628
+ elements: [
629
+ govcyResources.staticResources.elements.umdHasNoData.header,
630
+ govcyResources.staticResources.elements.umdHasNoData.instructions,
631
+ {
632
+ element: "htmlElement",
633
+ params: {
634
+ text: continueButtonText
635
+ }
636
+ }
637
+ ]
638
+ }
639
+ }
640
+ ]
641
+ }
642
+ ]
643
+ };
644
+
645
+ return pageTemplate;
646
+ }
647
+
648
+ /**
649
+ * Creates the page template for the updateMyDetails manual input page
650
+ * @param {string} siteId The site id
651
+ * @param {string} lang The language
652
+ * @param {object} page The page object
653
+ * @param {object} req The request object
654
+ * @returns {object} The page template
655
+ */
656
+ export function createUmdManualPageTemplate(siteId, lang, page, req) {
657
+ const umdConfig = page?.updateMyDetails || {};
658
+ // Build hub template
659
+ const pageTemplate = {
660
+ sections: [
661
+ {
662
+ name: "main",
663
+ elements: [
664
+ {
665
+ element: "form",
666
+ params: {
667
+ action: govcyResources.constructPageUrl(siteId, `${page.pageData.url}/update-my-details-response`, (req.query.route === "review" ? "review" : "")),
668
+ method: "POST",
669
+ elements: [govcyResources.csrfTokenInput(req.csrfToken())]
670
+ }
671
+ }
672
+ ]
673
+ }
674
+ ]
675
+ };
676
+
677
+
678
+ // the continue button
679
+ let continueButton = {
680
+ element: "button",
681
+ params: {
682
+ // text: (
683
+ // // if no continue button is provided use the static resource
684
+ // umdConfig?.continueButtonText?.[lang]
685
+ // ? umdConfig.continueButtonText
686
+ // : govcyResources.staticResources.text.continue
687
+ // ),
688
+ text: govcyResources.staticResources.text.continue,
689
+ variant: "primary",
690
+ type: "submit"
691
+ }
692
+ }
693
+
694
+ // ➕ Add header and instructions
695
+ pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual["header"]);
696
+ pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual["instructions"]);
697
+ //for each element in the scope array
698
+ umdConfig?.scope.forEach(element => {
699
+ // ➕ Add the element
700
+ pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual[element]);
701
+
702
+ })
703
+ // ➕ Add the continue button
704
+ pageTemplate.sections[0].elements[0].params.elements.push(continueButton);
705
+
706
+ return pageTemplate;
707
+
708
+ }
709
+
710
+ /**
711
+ * Constructs the redirect URL for Update My Details
712
+ * @param {object} req The request object
713
+ * @param {string} userId The user id
714
+ * @param {string} umdBaseURL The Update My Details base URL
715
+ * @param {string} returnUrl (Optional) The return URL
716
+ * @returns {string} The redirect URL
717
+ */
718
+ export function constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, returnUrl = "") {
719
+ // Only allow certain hosts
720
+ const allowedHosts = UPDATE_MY_DETAILS_ALLOWED_HOSTS;
721
+
722
+ // Validate URL against allowed hosts
723
+ const parsed = new URL(umdBaseURL);
724
+ if (!allowedHosts.includes(parsed.hostname)) {
725
+ throw new Error("Invalid Update My Details URL");
726
+ }
727
+
728
+ if (returnUrl === "") {
729
+ // Construct return URL
730
+ returnUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`;
731
+ }
732
+
733
+ // Validate return URL for production only (HTTPS)
734
+ if (!returnUrl.startsWith("https://") && isProdOrStaging() === "production") {
735
+ throw new Error("Return URL must be HTTPS in production");
736
+ }
737
+ // Encode url args
738
+ const encodedReturnUrl = encodeURIComponent(Buffer.from(returnUrl).toString("base64"));
739
+ const encodedUserId = encodeURIComponent(Buffer.from(userId).toString("base64"));
740
+ const lang = req.globalLang || "el";
741
+
742
+ // Construct redirect URL
743
+ return `${umdBaseURL}/ReturnUrl/SetReturnUrl?Url=${encodedReturnUrl}&UserProfileId=${encodedUserId}&lang=${lang}`;
744
+ }