From 744707dd3ed28d4e31ff48553936a9835b241c67 Mon Sep 17 00:00:00 2001 From: Erik Roberts <7330807+robere2@users.noreply.github.com> Date: Fri, 5 Sep 2025 17:09:17 -0400 Subject: [PATCH] feat: Refactor SubmitContact index.mjs with more documentation --- api/SubmitContact/index.mjs | 126 ++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 69 deletions(-) diff --git a/api/SubmitContact/index.mjs b/api/SubmitContact/index.mjs index 77c6528..a83f8f2 100644 --- a/api/SubmitContact/index.mjs +++ b/api/SubmitContact/index.mjs @@ -1,7 +1,16 @@ import fetch from "node-fetch"; import FormData from "form-data"; -async function assess(token, ip) { +/** + * Fetch the reCAPTCHA score for the user's submission, as evaulated by Google. + * @param token {string} reCAPTCHA token to evaluate. + * @param ip {string|undefined} IP address of the user. + * @returns {Promise} JSON response from Google reCAPTCHA v3 verify API. This response is expected to have at + * least a floating point "score" value in the root level of the JSON. + * @see {@link https://developers.google.com/recaptcha/docs/verify} Google reCAPTCHA verify API documentation + * @see {@link https://developers.google.com/recaptcha/docs/v3} Google reCAPTCHA v3 verify API response definition + */ +async function fetchGoogleCaptchaScore(token, ip) { let body = `response=${token}&secret=${process.env.RecaptchaSecret}`; if (ip) { body += `&remoteip=${ip}`; @@ -13,6 +22,34 @@ async function assess(token, ip) { }); } +/** + * Generate an error message HTTP response for the client. This Response can be returned from an Azure Function + * directly. + * @param code {number} HTTP status code to return. + * @param message {string} Error message to return. + * @returns {{httpResponse: {status: number, body: {error: string}}}} + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response} JavaScript HTTP Response object definition + */ +function createError(code, message) { + return { + httpResponse: { + status: code, + body: { error: message }, + }, + }; +} + +/** + * Send an email using Mailgun API. + * @param name {string} Name of the person sending the email. + * @param email {string} Email address of the person sending the email. + * @param subject {string} Subject of the email. + * @param body {string} Body of the email. + * @returns {Promise} Promise that resolves when the request to Mailgun to send an email is complete. + * @throws Error if the email could not be sent. + * @see {@link https://documentation.mailgun.com/docs/mailgun/api-reference/send/mailgun/messages/post-v3--domain-name--messages} + * Mailgun API documentation + */ async function sendEmail(name, email, subject, body) { const form = new FormData(); form.append("from", `${name} <${process.env.SenderEmail}>`); @@ -39,12 +76,7 @@ async function sendEmail(name, email, subject, body) { export default async function (context, req) { if (!req.body) { - return { - httpResponse: { - status: 400, - body: { error: "Missing contact form submission." }, - }, - }; + return createError(400, "Missing contact form submission."); } let name = req.body.name; @@ -54,69 +86,37 @@ export default async function (context, req) { let token = req.body.token; if (!name || !email || !subject || !body) { - return { - httpResponse: { - status: 400, - body: { error: "Missing required fields." }, - }, - }; + return createError(400, "Missing required fields."); } if (!token) { - return { - httpResponse: { - status: 401, - body: { error: "Missing Google Recaptcha token" }, - }, - }; + return createError(401, "Missing Google Recaptcha token."); } - let assessment; let score; try { - assessment = await assess(token, req.headers["x-forwarded-for"]); + const assessment = await fetchGoogleCaptchaScore( + token, + req.headers["x-forwarded-for"] + ); const body = await assessment.json(); if (!body.success) { - context.log( - "Received reCAPTCHA error codes: " + body["error-codes"].join(", ") - ); - return { - httpResponse: { - status: 500, - body: { - error: - "Received reCAPTCHA error codes: " + - body["error-codes"].join(", "), - }, - }, - }; + const errorMessage = + "Received reCAPTCHA error codes: " + body["error-codes"].join(", "); + context.log(errorMessage); + return createError(500, errorMessage); } if (body.action !== "contactSubmit") { context.log("Invalid reCAPTCHA action received"); - return { - httpResponse: { - status: 400, - body: { error: "Invalid reCAPTCHA action" }, - }, - }; + return createError(400, "Invalid reCAPTCHA action received"); } score = body.score; } catch (err) { context.log(err); - return { - httpResponse: { - status: 500, - body: { error: "reCAPTCHA error" }, - }, - }; + return createError(500, "reCAPTCHA error"); } if (score < 0.7) { - return { - httpResponse: { - status: 403, - body: { error: "Recaptcha failed. Try again, or come back later." }, - }, - }; + return createError(403, "Recaptcha failed. Try again, or come back later."); } name = name.substring(0, 300); @@ -125,29 +125,17 @@ export default async function (context, req) { body = body.substring(0, 2000); if (subject.length < 5 || body.length < 20) { - return { - httpResponse: { - status: 400, - body: { - error: - "subject must be between 5 and 100 characters, and body must be between 20 and 2000 characters.", - }, - }, - }; + return createError( + 400, + "subject must be between 5 and 100 characters, and body must be between 20 and 2000 characters." + ); } try { await sendEmail(name, email, subject, body); } catch (err) { console.error(err); - return { - httpResponse: { - status: 500, - body: { - error: "Failed to send email. Please try again later.", - }, - }, - }; + return createError(500, "Failed to send email. Please try again later."); } return {