UNPKG

@gov-cy/govcy-express-services

Version:

An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.

797 lines (704 loc) 37 kB
/** * Update My Details (UMD) page handler * Variants: * 1️⃣ Manual form — for non-eligible users (no access to UMD) * 2️⃣ Confirmation radio — eligible user with existing details * 3️⃣ External link — eligible user with no details (redirect to UMD service) * * GET: Determines variant and builds page template * POST: Validates form, stores data (variant 1/2), or redirects to UMD (variant 2/no) */ import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs"; import { getEnvVariable, getEnvVariableBool, isProdOrStaging } from "../utils/govcyEnvVariables.mjs"; import * as govcyResources from "../resources/govcyResources.mjs"; import * as dataLayer from "../utils/govcyDataLayer.mjs"; import { logger } from '../utils/govcyLogger.mjs'; import { handleMiddlewareError, dateStringISOtoDMY } from "../utils/govcyUtils.mjs"; import { govcyApiRequest } from "../utils/govcyApiRequest.mjs"; import { isAgeUnder, isValidCypriotCitizen, validateFormElements } from "../utils/govcyValidator.mjs"; import { populateFormData, getFormData } from "../utils/govcyFormHandling.mjs"; import { evaluatePageConditions } from "../utils/govcyExpressions.mjs"; import { tempSaveIfConfigured } from "../utils/govcyTempSave.mjs"; import { UPDATE_MY_DETAILS_ALLOWED_HOSTS } from "../utils/govcyConstants.mjs"; export async function govcyUpdateMyDetailsHandler(req, res, next, page, serviceCopy) { try { const { siteId, pageUrl } = req.params; const umdConfig = page?.updateMyDetails; // Sanity checks if ( !umdConfig || // updateMyDetails missing !umdConfig.scope || // scope missing !Array.isArray(umdConfig.scope) || // scope not an array umdConfig.scope.length === 0 || // scope empty !umdConfig.APIEndpoint || // APIEndpoint missing !umdConfig.APIEndpoint.url || // APIEndpoint.url missing !umdConfig.APIEndpoint.clientKey || // clientKey missing !umdConfig.APIEndpoint.serviceId || // serviceId missing !umdConfig.updateMyDetailsURL // updateMyDetailsURL missing ) { logger.debug("🚨 Invalid updateMyDetails configuration", req); return handleMiddlewareError( "🚨 Invalid updateMyDetails configuration", 500, next ); } // Environment vars const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false); let url = getEnvVariable(umdConfig.APIEndpoint.url || "", false); const clientKey = getEnvVariable(umdConfig.APIEndpoint.clientKey || "", false); const serviceId = getEnvVariable(umdConfig.APIEndpoint.serviceId || "", false); const dsfGtwKey = getEnvVariable(umdConfig?.APIEndpoint?.dsfgtwApiKey || "", ""); const method = (umdConfig?.APIEndpoint?.method || "GET").toLowerCase(); const umdBaseURL = getEnvVariable(umdConfig?.updateMyDetailsURL || "", ""); // Check if the upload API is configured correctly if (!url || !clientKey || !umdBaseURL) { return handleMiddlewareError(`Missing environment variables for updateMyDetails`, 500, next); } // Build hub template let pageTemplate = {}; // Get the user const user = dataLayer.getUser(req.session); let pageVariant = 0; // Check if the user is a cypriot if (!isValidCypriotCitizen(user)) { // --------------- Not eligible for Update my details --------------- // --------------- Page variant 1 pageVariant = 1; // load the manual input page pageTemplate = createUmdManualPageTemplate(siteId, serviceCopy.site.lang, page, req); } else { // --------------- Eligible for Update my details --------------- // Construct the URL with the language url += `/${serviceCopy.site.lang}`; // run the API request to check if the user has already uploaded their details // Perform the upload request const response = await govcyApiRequest( method, url, {}, true, user, { accept: "text/plain", "client-key": clientKey, "service-id": serviceId, ...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey }) }, 3, allowSelfSignedCerts ); // If not succeeded, handle error if (!response?.Succeeded) { return handleMiddlewareError(`updateMyDetailsAPIEndpoint - returned succeeded false`, 500, next); } // Check if the response contains the expected data if (!response?.Data || !response?.Data?.dob) { return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data`, 500, next); } // calculate if person in under 16 based on date of birth if (isAgeUnder(response.Data.dob, 16)) { // --------------- Not eligible for Update my details --------------- // --------------- Page variant 1 pageVariant = 1; // load the manual input page pageTemplate = createUmdManualPageTemplate(siteId, serviceCopy.site.lang, page, req); } else { let hasData = true; let userDetails = {}; //for each element in the scope array for (const element of umdConfig?.scope || []) { // The key in let key = element; // Get the value let value = response.Data?.[key] || ""; // Special case for address if (element === "address") { key = "addressInfo"; //if response.Data.addressInfo is an array if (response.Data.addressInfo && Array.isArray(response.Data.addressInfo)) { value = response.Data?.addressInfo?.[0]?.addressText || ""; } // else if response.Data.addressInfoUnstructured is not null and is an array else if (response.Data.addressInfoUnstructured && Array.isArray(response.Data.addressInfoUnstructured)) { value = response.Data?.addressInfoUnstructured?.[0]?.addressText || ""; } // else if response.Data.poBoxAddress is not null and is not an array else if (response.Data.poBoxAddress && Array.isArray(response.Data.poBoxAddress)) { value = response.Data?.poBoxAddress?.[0]?.poBoxText || ""; } else { value = ""; } } // Check if the key exists if (!Object.prototype.hasOwnProperty.call(response.Data || {}, key)) { hasData = false; return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data for element ${element}`, 500, next); } // Check if the value is null, undefined, or empty string if (value == null || value === "") { // Set hasData to false and set the value to an empty string hasData = false; userDetails[element] = ""; } else { // Set the value userDetails[element] = value; } } if (hasData) { // --------------- Page variant 2: Confirmation radio for eligible users with data pageVariant = 2; // load the has data page pageTemplate = createUmdHasDataPageTemplate(siteId, serviceCopy.site.lang, page, req, userDetails); } else { // --------------- Page variant 3: External redirect link for users with no data pageVariant = 3; // load the has no data page pageTemplate = createUmdHasNoDataPageTemplate(siteId, serviceCopy.site.lang, page, req, umdBaseURL); } } } // Deep copy pageTemplate to avoid modifying the original const pageTemplateCopy = JSON.parse(JSON.stringify(pageTemplate)); // if the page variant is 1 or 2 which means it has a form if (pageVariant === 1 || pageVariant === 2) { // Handle form data let theData = {}; //--------- Handle Validation Errors --------- // Check if validation errors exist in the session const validationErrors = dataLayer.getPageValidationErrors(req.session, siteId, pageUrl); if (validationErrors) { // Populate form data from validation errors theData = validationErrors?.formData || {}; } else { // Populate form data from session theData = dataLayer.getPageData(req.session, siteId, pageUrl); } //--------- End of Handle Validation Errors --------- populateFormData( pageTemplateCopy.sections[0].elements[0].params.elements, theData, validationErrors, req.session, siteId, pageUrl, req.globalLang, null, req.query.route); // if there are validation errors, add an error summary if (validationErrors?.errorSummary?.length > 0) { pageTemplateCopy.sections[0].elements[0].params.elements.unshift(govcyResources.errorSummary(validationErrors.errorSummary)); } } // Add topElements if provided if (Array.isArray(umdConfig.topElements)) { pageTemplateCopy.sections[0].elements[0].params.elements.unshift(...umdConfig.topElements); } //if hasBackLink == true add section beforeMain with backlink element if (umdConfig?.hasBackLink == true) { pageTemplateCopy.sections.unshift({ name: "beforeMain", elements: [ { element: "backLink", params: {} } ] }); } // Attach processed data to request req.processedPage = { pageData: { site: serviceCopy.site, pageData: { title: govcyResources.staticResources.text.updateMyDetailsTitle, layout: page?.pageData?.layout || "layouts/govcyBase.njk", mainLayout: page?.pageData?.mainLayout || "two-third" } }, pageTemplate: pageTemplateCopy }; logger.debug("Processed `govcyUpdateMyDetailsHandler` page data:", req.processedPage, req); next(); // Pass control to the next middleware or route } catch (error) { logger.debug("Error in govcyUpdateMyDetailsHandler middleware:", error.message); return next(error); // Pass the error to the next middleware } } /** * Middleware to handle page form submission for updateMyDetails */ export function govcyUpdateMyDetailsPostHandler() { return async (req, res, next) => { try { const { siteId, pageUrl } = req.params; // ⤵️ Load service and check if it exists const service = req.serviceData; // ⤵️ Find the current page based on the URL const page = getPageConfigData(service, pageUrl); if (!service || !page) { return handleMiddlewareError("Service or page data missing", 400, next); } // ----- Conditional logic comes here // ✅ Skip this POST handler if the page's conditions evaluate to true (redirect away) const conditionResult = evaluatePageConditions(page, req.session, siteId, req); if (conditionResult.result === false) { logger.debug("⛔️ Page condition evaluated to true on POST — skipping form save and redirecting:", conditionResult); return res.redirect(govcyResources.constructPageUrl(siteId, conditionResult.redirect)); } //----------------------------------------------------------------------------- // UpdateMyDetails configuration const umdConfig = page?.updateMyDetails; // Sanity checks if ( !umdConfig || // updateMyDetails missing !umdConfig.scope || // scope missing !Array.isArray(umdConfig.scope) || // scope not an array umdConfig.scope.length === 0 || // scope empty !umdConfig.APIEndpoint || // APIEndpoint missing !umdConfig.APIEndpoint.url || // APIEndpoint.url missing !umdConfig.APIEndpoint.clientKey || // clientKey missing !umdConfig.APIEndpoint.serviceId || // serviceId missing !umdConfig.updateMyDetailsURL // updateMyDetailsURL missing ) { logger.debug("🚨 Invalid updateMyDetails configuration", req); return handleMiddlewareError( "🚨 Invalid updateMyDetails configuration", 500, next ); } // Environment vars const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false); let url = getEnvVariable(umdConfig.APIEndpoint.url || "", false); const clientKey = getEnvVariable(umdConfig.APIEndpoint.clientKey || "", false); const serviceId = getEnvVariable(umdConfig.APIEndpoint.serviceId || "", false); const dsfGtwKey = getEnvVariable(umdConfig?.APIEndpoint?.dsfgtwApiKey || "", ""); const method = (umdConfig?.APIEndpoint?.method || "GET").toLowerCase(); const umdBaseURL = getEnvVariable(umdConfig?.updateMyDetailsURL || "", ""); // Check if the upload API is configured correctly if (!url || !clientKey || !umdBaseURL) { return handleMiddlewareError(`Missing environment variables for updateMyDetails`, 500, next); } // Build hub template let pageTemplate = {}; // user details (for variant 2: Confirmation radio for eligible users with data) let userDetails = {}; // Get the user const user = dataLayer.getUser(req.session); let pageVariant = 0; // Check if the user is a cypriot if (!isValidCypriotCitizen(user)) { // --------------- Not eligible for Update my details --------------- // --------------- Page variant 1:Manual form for non-eligible users pageVariant = 1; // load the manual input page pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req); } else { // --------------- Eligible for Update my details --------------- // Construct the URL with the language url += `/${service.site.lang}`; // run the API request to check if the user has already uploaded their details // Perform the upload request const response = await govcyApiRequest( method, url, {}, true, user, { accept: "text/plain", "client-key": clientKey, "service-id": serviceId, ...(dsfGtwKey !== "" && { "dsfgtw-api-key": dsfGtwKey }) }, 3, allowSelfSignedCerts ); // If not succeeded, handle error if (!response?.Succeeded) { return handleMiddlewareError(`updateMyDetailsAPIEndpoint - returned succeeded false`, 500, next); } // Check if the response contains the expected data if (!response?.Data || !response?.Data?.dob) { return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data`, 500, next); } // calculate if person in under 16 based on date of birth if (isAgeUnder(response.Data.dob, 16)) { // --------------- Not eligible for Update my details --------------- // --------------- Page variant 1:Manual form for non-eligible users pageVariant = 1; // load the manual input page pageTemplate = createUmdManualPageTemplate(siteId, service.site.lang, page, req); } else { let hasData = true; //for each element in the scope array for (const element of umdConfig?.scope || []) { // The key in let key = element; // Get the value let value = response.Data?.[key] || ""; // Special case for address if (element === "address") { key = "addressInfo"; //if response.Data.addressInfo is an array if (response.Data.addressInfo && Array.isArray(response.Data.addressInfo)) { value = response.Data?.addressInfo?.[0]?.addressText || ""; } // else if response.Data.addressInfoUnstructured is not null and is an array else if (response.Data.addressInfoUnstructured && Array.isArray(response.Data.addressInfoUnstructured)) { value = response.Data?.addressInfoUnstructured?.[0]?.addressText || ""; } // else if response.Data.poBoxAddress is not null and is not an array else if (response.Data.poBoxAddress && Array.isArray(response.Data.poBoxAddress)) { value = response.Data?.poBoxAddress?.[0]?.poBoxText || ""; } else { value = ""; } } // Check if the key exists if (!Object.prototype.hasOwnProperty.call(response.Data || {}, key)) { hasData = false; return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Missing response data for element ${element}`, 500, next); } // Check if the value is null, undefined, or empty string if (value == null || value === "") { // Set hasData to false and set the value to an empty string hasData = false; userDetails[element] = ""; } else { if (element === "dob") { key = "dob"; // value = response.Data?.[key] || ""; // Store different for ${element}_day, ${element}_month, ${element}_year const [year, month, day] = value.split("-").map(Number); userDetails[`${element}_day`] = day; userDetails[`${element}_month`] = month; userDetails[`${element}_year`] = year; } else { // Set the value as it is userDetails[element] = value; } } } if (hasData) { // --------------- Page variant 2: Confirmation radio for eligible users with data pageVariant = 2; // load the has data page pageTemplate = createUmdHasDataPageTemplate(siteId, service.site.lang, page, req, userDetails); } else { // --------------- Page variant 3: External redirect link for users with no data return handleMiddlewareError(`updateMyDetailsAPIEndpoint - Unexpected POST for User that has no Update my details data.`, 400, next); } } } //----------------------------------------------------------------------------- // 🔍 Find the form definition inside `pageTemplate.sections` let formElement = null; for (const section of pageTemplate.sections) { formElement = section.elements.find(el => el.element === "form"); if (formElement) break; } if (!formElement) { return handleMiddlewareError("🚨 Form definition not found.", 500, next); } let nextPage = null; // const formData = req.body; // Submitted data const formData = getFormData(formElement.params.elements, req.body, req.session, siteId, pageUrl); // Submitted data // ☑️ Start validation from top-level form elements const validationErrors = validateFormElements(formElement.params.elements, formData); // ❌ Return validation errors if any exist if (Object.keys(validationErrors).length > 0) { logger.debug("🚨 Validation errors:", validationErrors, req); logger.info("🚨 Validation errors on:", req.originalUrl); // store the validation errors dataLayer.storePageValidationErrors(req.session, siteId, pageUrl, validationErrors, formData); //redirect to the same page with error summary return res.redirect(govcyResources.constructErrorSummaryUrl( govcyResources.constructPageUrl(siteId, page.pageData.url, (req.query.route === "review" ? "review" : "")) )); } if (pageVariant === 1) { // --------------- Page variant 1:Manual form for non-eligible users //⤴️ Store validated form data in session dataLayer.storePageData(req.session, siteId, pageUrl, formData); dataLayer.storePageUpdateMyDetails(req.session, siteId, pageUrl, formData); } else if (pageVariant === 2) { // --------------- Page variant 2: Confirmation radio for eligible users with data const userChoice = req.body?.useTheseDetails?.trim().toLowerCase(); if (userChoice === "yes") { //⤴️ Store validated form data in session dataLayer.storePageData(req.session, siteId, pageUrl, userDetails); dataLayer.storePageUpdateMyDetails(req.session, siteId, pageUrl, userDetails); } else if (userChoice === "no") { // construct the return url to go to `:siteId/:pageUrl` + `?route=` + `review` const returnUrl = `${req.protocol}://${req.get("host")}${govcyResources.constructPageUrl(siteId, page.pageData.url, (req.query.route === "review" ? "review" : ""))}`; // Get user profile id const userId = user?.sub || ""; // 🔄 User chose to update their details externally const redirectUrl = constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, returnUrl); logger.info("User opted to update details externally", { userId: user.sub, redirectUrl }); return res.redirect(redirectUrl); } else { // 🚨 Should never happen (defensive) return handleMiddlewareError("Invalid value for useTheseDetails", 400, next); } } // 🔄 Fire-and-forget temporary save (non-blocking) (async () => { try { await tempSaveIfConfigured(req.session, service, siteId); } catch (e) { /* already logged internally */ } })(); logger.debug("✅ Form submitted successfully:", dataLayer.getPageData(req.session, siteId, pageUrl), req); logger.info("✅ Form submitted successfully:", req.originalUrl); // 🔍 Determine next page (if applicable) for (const section of pageTemplate.sections) { const form = section.elements.find(el => el.element === "form"); if (form) { //handle review route if (req.query.route === "review") { nextPage = govcyResources.constructPageUrl(siteId, "review"); } else { nextPage = page.pageData.nextPage; //nextPage = form.params.elements.find(el => el.element === "button" && el.params?.prototypeNavigate)?.params.prototypeNavigate; } } } // ➡️ Redirect to the next page if defined, otherwise return success if (nextPage) { logger.debug("🔄 Redirecting to next page:", nextPage, req); // 🛠 Fix relative paths return res.redirect(govcyResources.constructPageUrl(siteId, `${nextPage.split('/').pop()}`)); } res.json({ success: true, message: "Form submitted successfully" }); } catch (error) { return next(error); // Pass error to govcyHttpErrorHandler } }; } /** * Creates the has data page template for users that have data in Update My Details * @param {string} siteId The site id * @param {string} lang The language * @param {object} page The page object * @param {object} req The request object * @param {Array} userDetails The user details * @returns {object} The page template */ function createUmdHasDataPageTemplate(siteId, lang, page, req, userDetails) { const umdConfig = page?.updateMyDetails || {}; // Build hub template const pageTemplate = { sections: [ { name: "main", elements: [ { element: "form", params: { action: govcyResources.constructPageUrl(siteId, `${page.pageData.url}/update-my-details-response`, (req.query.route === "review" ? "review" : "")), method: "POST", elements: [govcyResources.csrfTokenInput(req.csrfToken())] } } ] } ] }; // the continue button let continueButton = { element: "button", params: { // if no continue button is provided use the static resource // text: ( // umdConfig?.continueButtonText?.[lang] // ? umdConfig.continueButtonText // : govcyResources.staticResources.text.continue // ), text: govcyResources.staticResources.text.continue, variant: "primary", type: "submit" } } // ➕ Add header and instructions pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["header"]); pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["instructions"]); // ➕ The data summaryList let summaryList = { element: "summaryList", params: { items: [] } } //for each element in the scope array umdConfig?.scope.forEach(element => { let value = userDetails?.[element] || ""; //if element is dob if (element === "dob") { value = dateStringISOtoDMY(value); } // add the key and value to the summaryList summaryList.params.items.push({ key: govcyResources.staticResources.text.updateMyDetailsScopes[element], value: [ { element: "textElement", params: { type: "span", classes: "govcy-whitespace-pre-line", text: govcyResources.getSameMultilingualObject(null, value) } } ] }) }) // ➕ Add the element pageTemplate.sections[0].elements[0].params.elements.push(summaryList); // ➕ Add the question pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdHasData["question"]); // ➕ Add the continue button pageTemplate.sections[0].elements[0].params.elements.push(continueButton); return pageTemplate; } /** * Creates the has no data page template for users that have no data in Update My Details * @param {string} siteId The site id * @param {string} lang The language * @param {object} page The page object * @param {object} req The request object * @returns {object} The page template */ function createUmdHasNoDataPageTemplate(siteId, lang, page, req, umdBaseURL) { const umdConfig = page?.updateMyDetails || {}; // Get user const user = dataLayer.getUser(req.session); // Get user profile id const userId = user?.sub || ""; const redirectUrl = constructUpdateMyDetailsRedirect(req, userId, umdBaseURL) // deep copy the continue button const continueButtonText = JSON.parse(JSON.stringify(govcyResources.staticResources.text.continue)) // Replace label placeholders on page title for (const lang of Object.keys(continueButtonText)) { continueButtonText[lang] = `<a class="govcy-btn-primary" href="${redirectUrl}">${continueButtonText[lang]}</a>`; } // Build hub template const pageTemplate = { sections: [ { name: "main", elements: [ { element: "form", params: { elements: [ govcyResources.staticResources.elements.umdHasNoData.header, govcyResources.staticResources.elements.umdHasNoData.instructions, { element: "htmlElement", params: { text: continueButtonText } } ] } } ] } ] }; return pageTemplate; } /** * Creates the page template for the updateMyDetails manual input page * @param {string} siteId The site id * @param {string} lang The language * @param {object} page The page object * @param {object} req The request object * @param {boolean} fromReview Whether the page is from the review route * @returns {object} The page template */ export function createUmdManualPageTemplate(siteId, lang, page, req, fromReview = false) { const umdConfig = page?.updateMyDetails || {}; // Build hub template const pageTemplate = { sections: [ { name: "main", elements: [ { element: "form", params: { action: govcyResources.constructPageUrl(siteId, `${page.pageData.url}/update-my-details-response`, (req.query.route === "review" ? "review" : "")), method: "POST", elements: [govcyResources.csrfTokenInput(req.csrfToken())] } } ] } ] }; // the continue button let continueButton = { element: "button", params: { // text: ( // // if no continue button is provided use the static resource // umdConfig?.continueButtonText?.[lang] // ? umdConfig.continueButtonText // : govcyResources.staticResources.text.continue // ), text: govcyResources.staticResources.text.continue, variant: "primary", type: "submit" } } // ➕ Add header and instructions pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual["header"]); pageTemplate.sections[0].elements[0].params.elements.push(govcyResources.staticResources.elements.umdManual["instructions"]); //for each element in the scope array umdConfig?.scope.forEach(element => { // ➕ Add the element pageTemplate.sections[0].elements[0].params.elements.push( fromReview ? govcyResources.staticResources.elements.umdManualReview[element] : govcyResources.staticResources.elements.umdManual[element] ); }) // ➕ Add the continue button pageTemplate.sections[0].elements[0].params.elements.push(continueButton); return pageTemplate; } /** * Constructs the redirect URL for Update My Details * @param {object} req The request object * @param {string} userId The user id * @param {string} umdBaseURL The Update My Details base URL * @param {string} returnUrl (Optional) The return URL * @returns {string} The redirect URL */ export function constructUpdateMyDetailsRedirect(req, userId, umdBaseURL, returnUrl = "") { // Only allow certain hosts const allowedHosts = UPDATE_MY_DETAILS_ALLOWED_HOSTS; // Validate URL against allowed hosts const parsed = new URL(umdBaseURL); if (!allowedHosts.includes(parsed.hostname)) { throw new Error("Invalid Update My Details URL"); } if (returnUrl === "") { // Construct return URL returnUrl = `${req.protocol}://${req.get("host")}${req.originalUrl}`; } // Validate return URL for production only (HTTPS) if (!returnUrl.startsWith("https://") && isProdOrStaging() === "production") { throw new Error("Return URL must be HTTPS in production"); } // Encode url args const encodedReturnUrl = encodeURIComponent(Buffer.from(returnUrl).toString("base64")); const encodedUserId = encodeURIComponent(Buffer.from(userId).toString("base64")); const lang = req.globalLang || "el"; // Construct redirect URL return `${umdBaseURL}/ReturnUrl/SetReturnUrl?Url=${encodedReturnUrl}&UserProfileId=${encodedUserId}&lang=${lang}`; }