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.

353 lines (306 loc) 17.2 kB
import * as govcyResources from "../resources/govcyResources.mjs"; import * as dataLayer from "../utils/govcyDataLayer.mjs"; import { logger } from "../utils/govcyLogger.mjs"; import { pageContainsFileInput } from "../utils/govcyHandleFiles.mjs"; import { whatsIsMyEnvironment, getEnvVariable, getEnvVariableBool } from '../utils/govcyEnvVariables.mjs'; import { handleMiddlewareError } from "../utils/govcyUtils.mjs"; import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs"; import { evaluatePageConditions } from "../utils/govcyExpressions.mjs"; import { govcyApiRequest } from "../utils/govcyApiRequest.mjs"; import { URL } from "url"; /** * Middleware to handle the delete file page. * This middleware processes the delete file page, populates the question, and shows validation errors. */ export function govcyFileDeletePageHandler() { return (req, res, next) => { try { const { siteId, pageUrl, elementName } = req.params; // Create a deep copy of the service to avoid modifying the original let serviceCopy = req.serviceData; // ⤵️ Find the current page based on the URL const page = getPageConfigData(serviceCopy, pageUrl); // deep copy the page template to avoid modifying the original const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate)); // ----- 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)); } // Validate the field: Only allow delete if the page contains a fileInput with the given name const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName); if (!fileInputElement) { return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next); } // Validate if the file input has a label if (!fileInputElement?.params?.label) { return handleMiddlewareError(`File input [${elementName}] does not have a label`, 404, next); } // --- Resolve file element data (normal + multipleThings add/edit) --- const { index } = req.params; const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName, index); // Guard if still nothing found if (!elementData || !elementData.fileId || !elementData.sha256) { logger.info(`govcyFileDeletePageHandler: File data not found for element [${elementName}] on page [${pageUrl}] in site [${siteId}]. Redirecting to "${siteId}/${pageUrl}".`); return res.redirect(govcyResources.constructPageUrl(siteId, pageUrl, (req.query.route === "review" ? "review" : ""))) // return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next); } // Deep copy page title (so we don’t mutate template) const pageTitle = JSON.parse(JSON.stringify(govcyResources.staticResources.text.deleteFileTitle)); // Replace label placeholders on page title for (const lang of Object.keys(pageTitle)) { const labelForLang = fileInputElement.params.label[lang] || fileInputElement.params.label.el || ""; pageTitle[lang] = pageTitle[lang].replace("{{file}}", labelForLang); } // Deep copy renderer pageData from let pageData = JSON.parse(JSON.stringify(govcyResources.staticResources.rendererPageData)); // Handle isTesting pageData.site.isTesting = (whatsIsMyEnvironment() === "staging"); // Base page template structure let pageTemplate = { sections: [ { name: "beforeMain", elements: [govcyResources.staticResources.elements.backLink] } ] }; //contruct the warning if the file was uploaded more than once const warningSameFile = { element: "warning", params: { text: govcyResources.staticResources.text.deleteSameFileWarning, } }; const showSameFileWarning = dataLayer.isFileUsedInSiteInputDataAgain( req.session, siteId, elementData ); // Construct page title const pageRadios = { element: "radios", params: { id: "deleteFile", name: "deleteFile", legend: pageTitle, isPageHeading: true, classes: "govcy-mb-6",// only include the warning block when the file is referenced >1 times elements: showSameFileWarning ? [warningSameFile] : [], items: [ { value: "yes", text: govcyResources.staticResources.text.deleteYesOption }, { value: "no", text: govcyResources.staticResources.text.deleteNoOption } ] } }; //------------- // Build proper action based on context (normal / multiple add / multiple edit) let actionPath; if (typeof req.params.index !== "undefined") { // multiple edit actionPath = `${pageUrl}/multiple/edit/${req.params.index}/delete-file/${elementName}`; } else if (req.originalUrl.includes("/multiple/add")) { // multiple add actionPath = `${pageUrl}/multiple/add/delete-file/${elementName}`; } else { // normal page actionPath = `${pageUrl}/delete-file/${elementName}`; // if normal page but has multipleThings, block it if (page?.multipleThings) { return handleMiddlewareError(`Single mode delete file not allowed on multipleThings pages`, 404, next) } } // Construct submit button const formElement = { element: "form", params: { action: govcyResources.constructPageUrl(siteId, actionPath, (req.query.route === "review" ? "review" : "")), method: "POST", elements: [ pageRadios, { element: "button", params: { type: "submit", text: govcyResources.staticResources.text.continue } }, govcyResources.csrfTokenInput(req.csrfToken()) ] } }; // --------- Handle Validation Errors --------- // Check if validation errors exist in the request const validationErrors = []; let mainElements = []; if (req?.query?.hasError) { validationErrors.push({ link: '#deleteFile-option-1', text: govcyResources.staticResources.text.deleteFileValidationError }); mainElements.push(govcyResources.errorSummary(validationErrors)); formElement.params.elements[0].params.error = govcyResources.staticResources.text.deleteFileValidationError; } //--------- End Handle Validation Errors --------- // Add elements to the main section, the H1, summary list, the submit button and the JS mainElements.push(formElement); // Append generated summary list to the page template pageTemplate.sections.push({ name: "main", elements: mainElements }); //prepare pageData pageData.site = serviceCopy.site; pageData.pageData.title = pageTitle; // Attach processed page data to the request req.processedPage = { pageData: pageData, pageTemplate: pageTemplate }; logger.debug("Processed delete file page data:", req.processedPage); next(); } catch (error) { return next(error); // Pass error to govcyHttpErrorHandler } }; } /** * Middleware to handle delete file post form processing * This middleware processes the post, validates the form and handles the file data layer */ export function govcyFileDeletePostHandler() { return async (req, res, next) => { try { // Extract siteId and pageUrl from request let { siteId, pageUrl, elementName } = req.params; // get service data let serviceCopy = req.serviceData; // 🔍 Find the page by pageUrl const page = getPageConfigData(serviceCopy, pageUrl); // Deep copy pageTemplate to avoid modifying the original const pageTemplateCopy = JSON.parse(JSON.stringify(page.pageTemplate)); // ----- Conditional logic comes here // Check if the page has conditions and apply logic 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)); } // Validate the field: Only allow delete if the page contains a fileInput with the given name const fileInputElement = pageContainsFileInput(pageTemplateCopy, elementName); if (!fileInputElement) { return handleMiddlewareError(`File input [${elementName}] not allowed on this page`, 404, next); } // --- Resolve file element data (normal + multipleThings add/edit) --- const { index } = req.params; const elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName, index); // Guard if still nothing found if (!elementData || !elementData.fileId || !elementData.sha256) { return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next); } // Build proper action based on context (normal / multiple add / multiple edit) let actionPath; if (typeof req.params.index !== "undefined") { // multiple edit actionPath = `${pageUrl}/multiple/edit/${req.params.index}`; } else if (req.originalUrl.includes("/multiple/add")) { // multiple add actionPath = `${pageUrl}/multiple/add`; } else { // normal page actionPath = `${pageUrl}`; // if normal page but has multipleThings, block it if (page?.multipleThings) { return handleMiddlewareError(`Single mode delete file not allowed on multipleThings pages`, 404, next) } } // the page base return url const pageBaseReturnUrl = `http://localhost:3000/${siteId}/${actionPath}`; //check if input `deleteFile` has a value if (!req?.body?.deleteFile || (req.body.deleteFile !== "yes" && req.body.deleteFile !== "no")) { logger.debug("⛔️ No deleteFile value provided on POST — skipping form save and redirecting:", req.body); //construct the page url with error let myUrl = new URL(pageBaseReturnUrl + `/delete-file/${elementName}`); //check if the route is review if (req.query.route === "review") { myUrl.searchParams.set("route", "review"); } //set the error flag myUrl.searchParams.set("hasError", "1"); //redirect to the same page with error summary (relative path) return res.redirect(govcyResources.constructErrorSummaryUrl(myUrl.pathname + myUrl.search)); } //if no validation errors if (req.body.deleteFile === "yes") { // Try to delete the file via the delete API. // If it fails, log the error but continue to remove the file from the session try { // Get the delete file configuration const deleteCfg = serviceCopy?.site?.fileDeleteAPIEndpoint; // Check if download file configuration is available if (!deleteCfg?.url || !deleteCfg?.clientKey || !deleteCfg?.serviceId) { return handleMiddlewareError(`File delete APU configuration not found`, 404, next); } // Environment vars const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false); let url = getEnvVariable(deleteCfg.url || "", false); const clientKey = getEnvVariable(deleteCfg.clientKey || "", false); const serviceId = getEnvVariable(deleteCfg.serviceId || "", false); const dsfGtwKey = getEnvVariable(deleteCfg?.dsfgtwApiKey || "", ""); const method = (deleteCfg?.method || "GET").toLowerCase(); // Check if the upload API is configured correctly if (!url || !clientKey) { return handleMiddlewareError(`Missing environment variables for upload`, 404, next); } // Construct the URL with tag being the elementName url += `/${encodeURIComponent(elementData.fileId)}/${encodeURIComponent(elementData.sha256)}`; // Get the user const user = dataLayer.getUser(req.session); // Perform the delete 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) { logger.error("fileDeleteAPIEndpoint returned succeeded false"); } } catch (error) { logger.error(`fileDeleteAPIEndpoint Call failed: ${error.message}`); } // if succeeded all good // dataLayer.storePageDataElement(req.session, siteId, pageUrl, elementName, ""); dataLayer.removeAllFilesFromSite(req.session, siteId, { fileId: elementData.fileId, sha256: elementData.sha256 }); logger.info(`File deleted by user`, { siteId, pageUrl, elementName }); } // construct the page url let myUrl = new URL(pageBaseReturnUrl); //check if the route is review if (req.query.route === "review") { myUrl.searchParams.set("route", "review"); } // redirect to the page (relative path) res.redirect(myUrl.pathname + myUrl.search); } catch (error) { logger.error("Error in govcyFileDeletePostHandler middleware:", error.message); return next(error); // Pass error to govcyHttpErrorHandler } }; }