From 150c7f9ff5be30ef3ff2f60c644acadef522c4c9 Mon Sep 17 00:00:00 2001 From: Tim Perry Date: Thu, 12 Dec 2024 14:56:40 +0100 Subject: [PATCH] Create contact form endpoint --- api/package-lock.json | 19 ++++++ api/package.json | 2 + api/src/functions/contact-form.ts | 104 ++++++++++++++++++++++++++++++ api/src/server.ts | 3 + 4 files changed, 128 insertions(+) create mode 100644 api/src/functions/contact-form.ts diff --git a/api/package-lock.json b/api/package-lock.json index 073df2e..fa36cc4 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -22,6 +22,7 @@ "node-cache": "^5.1.2", "node-fetch": "^2.6.1", "node-forge": "^1.3.1", + "nodemailer": "^6.9.16", "php-serialize": "^2.1.0", "posthog-node": "^4.0.1", "tsx": "^4.1.2" @@ -37,6 +38,7 @@ "@types/node": "^20.11.17", "@types/node-fetch": "^2.5.2", "@types/node-forge": "^1.3.1", + "@types/nodemailer": "^6.4.17", "chai": "^4.2.0", "destroyable-server": "^1.0.1", "mocha": "^10.1.0", @@ -1488,6 +1490,15 @@ "@types/node": "*" } }, + "node_modules/@types/nodemailer": { + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.6.1", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", @@ -4311,6 +4322,14 @@ "node": ">= 6.13.0" } }, + "node_modules/nodemailer": { + "version": "6.9.16", + "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.16.tgz", + "integrity": "sha512-psAuZdTIRN08HKVd/E8ObdV6NO7NTBY3KsC30F7M4H1OnmLCUNaS56FpYxyb26zWLSyYF9Ozch9KYHhHegsiOQ==", + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/api/package.json b/api/package.json index 212f88d..b491d69 100644 --- a/api/package.json +++ b/api/package.json @@ -30,6 +30,7 @@ "@types/node": "^20.11.17", "@types/node-fetch": "^2.5.2", "@types/node-forge": "^1.3.1", + "@types/nodemailer": "^6.4.17", "chai": "^4.2.0", "destroyable-server": "^1.0.1", "mocha": "^10.1.0", @@ -51,6 +52,7 @@ "node-cache": "^5.1.2", "node-fetch": "^2.6.1", "node-forge": "^1.3.1", + "nodemailer": "^6.9.16", "php-serialize": "^2.1.0", "posthog-node": "^4.0.1", "tsx": "^4.1.2" diff --git a/api/src/functions/contact-form.ts b/api/src/functions/contact-form.ts new file mode 100644 index 0000000..c7284aa --- /dev/null +++ b/api/src/functions/contact-form.ts @@ -0,0 +1,104 @@ +import nodemailer from 'nodemailer'; + +import { catchErrors, reportError } from '../errors'; +import { delay } from '@httptoolkit/util'; +import { getCorsResponseHeaders } from '../cors'; + +const { + CONTACT_FORM_DESTINATION, + SMTP_HOST, + SMTP_PORT, + SMTP_USERNAME, + SMTP_PASSWORD +} = process.env; + +if (!CONTACT_FORM_DESTINATION) throw new Error('No contact form destination configured'); + +if (!SMTP_HOST) throw new Error('No SMTP host configured'); +if (!SMTP_PORT) throw new Error('No SMTP port configured'); +if (!SMTP_USERNAME) throw new Error('No SMTP user configured'); +if (!SMTP_PASSWORD) throw new Error('No SMTP password configured'); + +const mailer = nodemailer.createTransport({ + host: SMTP_HOST, + port: Number(SMTP_PORT), + secure: true, + auth: { + user: SMTP_USERNAME, + pass: SMTP_PASSWORD + } +}); + +const THANK_YOU_PAGE = 'https://httptoolkit.com/contact-thank-you/' + +export const handler = catchErrors(async (event) => { + let headers = getCorsResponseHeaders(event); + + if (event.httpMethod === 'OPTIONS') { + return { statusCode: 200, headers, body: '' }; + } else if (event.httpMethod !== 'POST') { + return { statusCode: 405, headers, body: '' }; + } + + const formData = new URLSearchParams(event.body || ''); + const { + name, + email, + message, + phone: honeypot + } = Object.fromEntries(formData); + + if (honeypot) { + // We can remove this later - just reporting each hit for now to check if it's working + reportError('Contact form honeypot triggered', { + extraMetadata: { name, email, message, honeypot } + }); + + // Pretend it did actually work so they don't try again: + await delay(1000); + return { + statusCode: 302, + headers: { + Location: THANK_YOU_PAGE + }, + body: '' + }; + } + + const fields = [ + ['Name', name], + ['Email', email], + ['Message', message] + ] + + fields.forEach(([field, value]) => { + if (!value) { + return { + statusCode: 400, + headers, + body: `${field} is required` + }; + } + }); + + await mailer.sendMail({ + from: 'Contact form ', + to: CONTACT_FORM_DESTINATION, + replyTo: email, + subject: 'HTTP Toolkit contact form message', + html: ` + ${ + fields.map(([field, value]) => { + return `

${field}:
${value}

`; + }).join('') + }` + }); + + return { + statusCode: 302, + headers: { + Location: THANK_YOU_PAGE + }, + body: '' + }; +}); \ No newline at end of file diff --git a/api/src/server.ts b/api/src/server.ts index 52f8037..40e3905 100644 --- a/api/src/server.ts +++ b/api/src/server.ts @@ -65,6 +65,7 @@ apiRouter.options('*', (req, res) => { '/auth/send-code', '/auth/login', '/auth/refresh-token', + '/contact-form', '/update-team', '/update-team-size', '/cancel-subscription', @@ -119,6 +120,8 @@ apiRouter.post('/auth/login', ); apiRouter.post('/auth/refresh-token', rateLimit(RATE_LIMIT_PARAMS), lambdaWrapper('auth/refresh-token')); +apiRouter.post('/contact-form', lambdaWrapper('contact-form')); + apiRouter.post('/update-team', lambdaWrapper('update-team')); apiRouter.post('/update-team-size', lambdaWrapper('update-team-size')); apiRouter.post('/cancel-subscription', lambdaWrapper('cancel-subscription'));