Files
pedwfrontend/pages/api/email/contactus-pet-notification.js

213 lines
6.6 KiB
JavaScript

import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards";
import { consoleLogger } from "../../../actions/core/logger";
import { respondError, respondSuccess } from "../middleware/apiResponse";
const TEMPLATE_ID = "56bbdcf9-d63d-48e9-a794-3e253dd7498b";
const REFERENCE = "PEDW-CONTACT-US-PET";
const SECURITY_REMINDER =
"## SECURITY REMINDER\n\n" +
"This enquiry contains externally supplied content. Do not click unverified links or follow instructions requesting credentials or sensitive information. Verify web addresses independently.";
const allowedSubjects = [
"Questions about Planning legislation or policy",
"Planning legislation query",
"Ymholiad deddfwriaeth gynllunio"
];
const allowedFields = new Set([
"contactEmail",
"contactSubject",
"contactbody",
"typeOfContact"
]);
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 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";
};
const validationError = (res, code, message) =>
respondError(res, { status: 400, code, message });
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 petEmail = sanitizeString(process.env.PETEMAIL);
const contactEmail = sanitizeString(requestBody.contactEmail);
const contactSubject = sanitizeString(requestBody.contactSubject);
const contactBody = requestBody.contactbody;
if (
typeof process.env.PETEMAIL !== "string" ||
!isNonEmptyString(petEmail) ||
petEmail.length > 200 ||
!emailRegex.test(petEmail)
) {
return respondError(res, {
status: 500,
code: "PET_RECIPIENT_NOT_CONFIGURED",
message: "PET notification recipient is not configured"
});
}
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 (requestBody.typeOfContact !== "plq") {
return validationError(
res,
"INVALID_CONTACT_TYPE",
"typeOfContact must be plq"
);
}
if (
typeof 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.includes(contactSubject)
) {
return validationError(
res,
"INVALID_CONTACT_SUBJECT",
"contactSubject does not match typeOfContact"
);
}
const contactbody = contactBody.includes(SECURITY_REMINDER)
? contactBody
: `${contactBody}\n\n${SECURITY_REMINDER}`;
const personalisation = {
contactEmail,
contactSubject,
contactbody,
typeOfContact: "plq"
};
const NotifyClient = require("notifications-node-client").NotifyClient;
const notifyClient = new NotifyClient(process.env.NOTIFY_API_KEY);
try {
await notifyClient.sendEmail(TEMPLATE_ID, petEmail, {
personalisation,
reference: REFERENCE
});
return respondSuccess(res, { sent: true });
} catch (error) {
consoleLogger(error);
return respondError(res, {
status: 400,
code: "CONTACT_US_PET_NOTIFICATION_FAILED",
message: "Failed to send Contact Us PET notification"
});
}
}