@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.
186 lines (157 loc) • 9.01 kB
JavaScript
import { getPageConfigData } from "../utils/govcyLoadConfigData.mjs";
import { evaluatePageConditions } from "../utils/govcyExpressions.mjs";
import { getEnvVariable, getEnvVariableBool } from "../utils/govcyEnvVariables.mjs";
import { ALLOWED_FILE_MIME_TYPES } from "../utils/govcyConstants.mjs";
import { govcyApiRequest } from "../utils/govcyApiRequest.mjs";
import * as dataLayer from "../utils/govcyDataLayer.mjs";
import { logger } from '../utils/govcyLogger.mjs';
import { handleMiddlewareError } from "../utils/govcyUtils.mjs";
import { isMagicByteValid, pageContainsFileInput } from "../utils/govcyHandleFiles.mjs";
export function govcyFileViewHandler() {
return async (req, res, next) => {
try {
const { siteId, pageUrl, elementName } = req.params;
// Detect MultipleThings modes based on URL
let mode = "single";
let index = null;
if (req.originalUrl.includes("/multiple/add")) {
mode = "multipleThingsDraft";
} else if (req.originalUrl.includes("/multiple/edit/")) {
mode = "multipleThingsEdit";
index = parseInt(req.params.index, 10);
}
// Create a deep copy of the service to avoid modifying the original
let serviceCopy = req.serviceData;
// Get the download file configuration
const downloadCfg = serviceCopy?.site?.fileDownloadAPIEndpoint;
// Check if download file configuration is available
if (!downloadCfg?.url || !downloadCfg?.clientKey || !downloadCfg?.serviceId) {
return handleMiddlewareError(`File download APU configuration not found`, 404, next);
}
// Environment vars
const allowSelfSignedCerts = getEnvVariableBool("ALLOW_SELF_SIGNED_CERTIFICATES", false);
let url = getEnvVariable(downloadCfg.url || "", false);
const clientKey = getEnvVariable(downloadCfg.clientKey || "", false);
const serviceId = getEnvVariable(downloadCfg.serviceId || "", false);
const dsfGtwKey = getEnvVariable(downloadCfg?.dsfgtwApiKey || "", "");
const method = (downloadCfg?.method || "GET").toLowerCase();
// Check if the upload API is configured correctly
if (!url || !clientKey) {
return handleMiddlewareError(`Missing environment variables for upload`, 404, next);
}
// ⤵️ 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 handleMiddlewareError(`Page condition evaluated to true on POST — skipping form save and redirecting`, 404, next);
// }
// If mode is `single` make sure it has no multipleThings
if (mode === "single" && page?.multipleThings) {
return handleMiddlewareError(`Single mode file view not allowed on multipleThings pages`, 404, next)
}
// 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);
}
//check the reference number
const referenceNo = dataLayer.getSiteLoadDataReferenceNumber(req.session, siteId);
if (!referenceNo) {
return handleMiddlewareError(`Missing submission reference number`, 404, next);
}
//get element data
let elementData;
if (mode === "single") {
elementData = dataLayer.getFormDataValue(req.session, siteId, pageUrl, elementName);
} else if (mode === "multipleThingsDraft") {
elementData = dataLayer.getMultipleDraft(req.session, siteId, pageUrl)?.[elementName];
} else if (mode === "multipleThingsEdit") {
const items = dataLayer.getPageData(req.session, siteId, pageUrl) || [];
elementData = items[index]?.[elementName];
}
// If the element data is not found, return an error response
if (!elementData || !elementData?.sha256 || !elementData?.fileId) {
return handleMiddlewareError(`File input [${elementName}] data not found on this page`, 404, next);
}
// Construct the URL with tag being the elementName
url += `/${encodeURIComponent(referenceNo)}/${encodeURIComponent(elementData.fileId)}/${encodeURIComponent(elementData.sha256)}`;
// Get the user
const user = dataLayer.getUser(req.session);
// 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(`fileDownloadAPIEndpoint returned succeeded false`, 500, next);
}
// Check if the response contains the expected data
if (!response?.Data?.contentType || !response?.Data?.fileName || !response?.Data?.base64) {
return handleMiddlewareError(`fileDownloadAPIEndpoint - Missing contentType, fileName or base64 in response data`, 500, next);
}
// Get filename
const filename = response?.Data?.fileName || "filename";
const fallbackFilename = asciiFallback(filename);
const utf8Filename = encodeRFC5987(filename);
// Decode base64 to binary
const fileBuffer = Buffer.from(response.Data.base64, 'base64');
// file type checks
// 1. Check declared mimetype
if (!ALLOWED_FILE_MIME_TYPES.includes(response?.Data?.contentType)) {
return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (MIME not allowed)`, 500, next);
}
// 2. Check actual file content (magic bytes) matches claimed MIME type
if (!isMagicByteValid(fileBuffer, response?.Data?.contentType)) {
return handleMiddlewareError(`fileDownloadAPIEndpoint - Invalid file type (magic byte mismatch)`, 500, next);
}
// Check if Buffer is empty
if (!fileBuffer || fileBuffer.length === 0) {
return handleMiddlewareError(`fileDownloadAPIEndpoint - File is empty or invalid`, 500, next);
}
// Send the file to the browser
res.type(response?.Data?.contentType);
res.setHeader(
'Content-Disposition',
`inline; filename="${fallbackFilename}"; filename*=UTF-8''${utf8Filename}`
);
res.send(fileBuffer);
} catch (error) {
logger.error("Error in govcyViewFileHandler middleware:", error.message);
return next(error); // Pass the error to the next middleware
}
};
}
//------------------------------------------------------------------------------
// Helper functions
// rfc5987 encoding for filename*
function encodeRFC5987(str) {
return encodeURIComponent(str)
.replace(/['()*]/g, c => '%' + c.charCodeAt(0).toString(16).toUpperCase())
.replace(/%(7C|60|5E)/g, (m, p) => '%' + p); // | ` ^
}
// ASCII fallback for old browsers
function asciiFallback(str) {
return str
.replace(/[^\x20-\x7E]/g, '') // strip non-ASCII
.replace(/[/\\?%*:|"<>]/g, '-') // reserved chars
.replace(/[\r\n]/g, ' ') // drop newlines
.replace(/"/g, "'") // no quotes
|| 'download';
}