import axios from "axios"; import { isNonEmptyString, sanitizeString } from "../../../actions/core/guards"; import { consoleLogger } from "../../../actions/core/logger"; import { getToken } from "../../../actions/core/token"; import { hashAPIPath } from "../../../actions/core/hash"; import { respondError, respondSuccess } from "../middleware/apiResponse"; const WEBAPI_URL = process.env.RELAY_ROOT || "https://dev-pedw-ns.servicebus.windows.net/dev-pedw-hc/"; const SECURITY_REMINDER = "--- SECURITY REMINDER ---\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 teamMap = { plq: "ec48450c-7e26-ec11-a97f-00224800e98c", //PET ipq: "7fb9e598-7d26-ec11-a97f-00224800e98c", // Infrastructure pcpq: "8ffabad9-7c26-ec11-a97f-00224800e98c", // Casework pi: "5554882f-4606-ec11-a97f-00224800d374", // Digital Support Team appadvice: "cc3d987e-7c26-ec11-a97f-00224800e98c", repadvice: "cc3d987e-7c26-ec11-a97f-00224800e98c", ceq: "4fddd7e8-7d26-ec11-a97f-00224800e98c", // Inspector Scheduling other: "8ffabad9-7c26-ec11-a97f-00224800e98c", // Casework postdec: "1357e457-7e26-ec11-a97f-00224800e98c", // SLT Team comp: "1357e457-7e26-ec11-a97f-00224800e98c" // SLT Team }; 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" ]); 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]:.+)])$/; 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 }); 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 createTaskBody = req.body; if ( createTaskBody === null || typeof createTaskBody !== "object" || Array.isArray(createTaskBody) ) { return validationError( res, "INVALID_REQUEST_BODY", "Request body must be a JSON object" ); } if (Object.keys(createTaskBody).some((field) => !allowedFields.has(field))) { return validationError( res, "UNSUPPORTED_REQUEST_FIELD", "Request contains an unsupported field" ); } const contactEmail = sanitizeString(createTaskBody?.contactEmail); const contactSubject = sanitizeString(createTaskBody?.contactSubject); const contactBody = sanitizeString(createTaskBody?.contactbody); if ( typeof createTaskBody.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 createTaskBody.typeOfContact !== "string" || !Object.prototype.hasOwnProperty.call(teamMap, createTaskBody.typeOfContact) ) { return validationError( res, "INVALID_CONTACT_TYPE", "typeOfContact is not supported" ); } if ( typeof createTaskBody.contactbody !== "string" || !isNonEmptyString(contactBody) || contactBody.length > 800 ) { return validationError( res, "INVALID_CONTACT_BODY", "contactbody must contain between 1 and 800 characters" ); } const contactValue = createTaskBody.typeOfContact; if ( typeof createTaskBody.contactSubject !== "string" || !allowedSubjects[contactValue].includes(contactSubject) ) { return validationError( res, "INVALID_CONTACT_SUBJECT", "contactSubject does not match typeOfContact" ); } const queryUrl = "tasks"; const token = await getToken(); let teamId = teamMap[contactValue]; const payload = { "subject": `Contact Us Enquiry - ${contactSubject}`, "description": `From: ${contactEmail}\n\n${contactBody}\n\n${SECURITY_REMINDER}`, "scheduledstart": new Date().toISOString(), "scheduledend": new Date( new Date().getTime() + 60 * 60000 * 24 ).toISOString(), // 30 mins ...(teamId && { "ownerid@odata.bind": `/teams(${teamId})` }) }; //console.log("/////Create Task:\n", payload, "\n//////////////"); const config = { method: "post", url: WEBAPI_URL + queryUrl + hashAPIPath(queryUrl), headers: { "OData-MaxVersion": "4.0", "OData-Version": "4.0", "Accept": "application/json", "Prefer": 'odata.include-annotations="*",return=representation', "Authorization": "Bearer " + token.access_token, "Content-Type": "application/json" }, data: JSON.stringify(payload) }; //console.log("/////Create Task config:\n", config, "\n//////////////"); try { const { data } = await axios(config); return respondSuccess(res, data); } catch (error) { consoleLogger(error); return respondError(res, { status: 400, code: "CRM_TASK_CREATE_FAILED", message: "Failed to create CRM task" }); } }