Files
pedwfrontend/pages/api/email/contactus-confirmation.js

247 lines
7.7 KiB
JavaScript

import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards";
import { consoleLogger } from "../../../actions/core/logger";
import removeContactUsEmailLinks from "../../../lib/security/contactus-email-links";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const TEMPLATE_IDS = {
en: "69228c5f-8500-40ff-a6c5-95a9434c3ec8",
cy: "b8f1f76d-4ab9-4a2f-b294-e3623c6095a3"
};
const REFERENCE = "PEDW-CONTACT-US";
const allowedSubjects = {
plq: [
"Questions about Planning legislation or policy",
"Planning legislation query",
"Ymholiad deddfwriaeth gynllunio"
],
ipq: [
"Questions or comments about applications for major infrastructure projects",
"Infrastructure procedure query",
"Ymholiad gweithdrefn seilwaith"
],
pcpq: [
"Questions or comments about planning and environmental appeals",
"Planning casework procedure query",
"Ymholiad gweithdrefn gwaith achos cynllunio"
],
pi: ["Query about using the Casework Portal", "Portal Issues", "Materion Porth"],
appadvice: [
"Advice How to submit an appeal",
"Appeal submission advice",
"Cyngor cyflwyno apêl"
],
repadvice: [
"Advice How to comment on an appeal or application",
"Representation submission advice",
"Cyngor cyflwyno sylwadau"
],
ceq: [
"Queries about forthcoming Hearing or Inquiry event",
"Casework Events Query",
"Ymholiad Digwyddiadau Gwaith Achos"
],
other: ["Other enquiry for PEDW", "Any Other Enquiries", "Unrhyw Ymholiadau Eraill"],
postdec: [
"Issues or questions about a case after the decision is issued",
"Post Decision",
"Ar ôl Penderfyniad"
],
comp: [
"General Complaints of Compliments",
"Complaints/Complements",
"Cwynion/Canmoliaethau"
]
};
const allowedFields = new Set([
"contactEmail",
"contactSubject",
"contactbody",
"typeOfContact",
"language"
]);
const emailRegex =
/^(?!.*(?:.+\.)?(?:burpcollaborator\.net|interactsh\.com|mailinator\.com|10minutemail\.com|yopmail\.com|trashmail\.com)$)(?:[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}|(?:\[(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?|[a-zA-Z0-9-]*[a-zA-Z0-9]:.+)])$/;
// Origin checking is browser defence in depth only. It is not authentication and
// does not prevent a direct HTTP client from attempting abuse.
const hasTrustedOrigin = (req) => {
const requestOrigin = req?.headers?.origin;
if (typeof requestOrigin !== "string" || requestOrigin === "null") {
return false;
}
try {
const requestUrl = new URL(requestOrigin);
if (requestOrigin !== requestUrl.origin) {
return false;
}
const trustedOrigins = [process.env.NEXTAUTH_URL, process.env.CY_API_ROOT]
.filter(
(configuredUrl) =>
typeof configuredUrl === "string" && configuredUrl.length > 0
)
.map((configuredUrl) => new URL(configuredUrl).origin);
return trustedOrigins.includes(requestUrl.origin);
} catch (_error) {
return false;
}
};
const validationError = (res, code, message) =>
respondError(res, { status: 400, code, message });
const isPlainObject = (value) => {
if (value === null || typeof value !== "object" || Array.isArray(value)) {
return false;
}
const prototype = Object.getPrototypeOf(value);
return prototype === null || prototype?.constructor?.name === "Object";
};
export default async function ApiProxy(req, res) {
if (req.method !== "POST") {
res.setHeader("Allow", "POST");
return respondError(res, {
status: 405,
code: "METHOD_NOT_ALLOWED",
message: "Method not allowed"
});
}
const contentType = req.headers?.["content-type"];
if (
typeof contentType !== "string" ||
contentType.split(";", 1)[0].trim().toLowerCase() !== "application/json"
) {
return respondError(res, {
status: 415,
code: "UNSUPPORTED_MEDIA_TYPE",
message: "Content-Type must be application/json"
});
}
if (!hasTrustedOrigin(req)) {
return respondError(res, {
status: 403,
code: "FORBIDDEN",
message: "Request not permitted"
});
}
const requestBody = req.body;
if (!isPlainObject(requestBody)) {
return validationError(
res,
"INVALID_REQUEST_BODY",
"Request body must be a JSON object"
);
}
const requestFields = Object.keys(requestBody);
if (
requestFields.length !== allowedFields.size ||
requestFields.some((field) => !allowedFields.has(field))
) {
return validationError(
res,
"INVALID_REQUEST_FIELDS",
"Request must contain exactly the supported fields"
);
}
const contactEmail = sanitizeString(requestBody.contactEmail);
const contactSubject = sanitizeString(requestBody.contactSubject);
const contactBody = sanitizeString(requestBody.contactbody);
const typeOfContact = requestBody.typeOfContact;
const language = requestBody.language;
if (
typeof requestBody.contactEmail !== "string" ||
!isNonEmptyString(contactEmail) ||
contactEmail.length > 200 ||
!emailRegex.test(contactEmail)
) {
return validationError(
res,
"INVALID_CONTACT_EMAIL",
"contactEmail must be a valid email address of 200 characters or fewer"
);
}
if (
typeof typeOfContact !== "string" ||
!Object.prototype.hasOwnProperty.call(allowedSubjects, typeOfContact)
) {
return validationError(
res,
"INVALID_CONTACT_TYPE",
"typeOfContact is not supported"
);
}
if (
typeof requestBody.contactbody !== "string" ||
!isNonEmptyString(contactBody) ||
contactBody.length > 800
) {
return validationError(
res,
"INVALID_CONTACT_BODY",
"contactbody must contain between 1 and 800 characters"
);
}
if (
typeof requestBody.contactSubject !== "string" ||
!allowedSubjects[typeOfContact].includes(contactSubject)
) {
return validationError(
res,
"INVALID_CONTACT_SUBJECT",
"contactSubject does not match typeOfContact"
);
}
if (language !== "en" && language !== "cy") {
return validationError(
res,
"INVALID_LANGUAGE",
"language must be either en or cy"
);
}
const personalisation = {
contactEmail,
contactSubject,
contactbody: removeContactUsEmailLinks(contactBody),
typeOfContact
};
const NotifyClient = require("notifications-node-client").NotifyClient;
const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY);
try {
await notifyClient.sendEmail(TEMPLATE_IDS[language], contactEmail, {
personalisation,
reference: REFERENCE
});
return respondSuccess(res, { sent: true });
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CONTACT_US_CONFIRMATION_FAILED",
message: "Failed to send Contact Us confirmation email"
});
}
}