Skip to content

Commit 150c7f9

Browse files
committed
Create contact form endpoint
1 parent 972c8a1 commit 150c7f9

File tree

4 files changed

+128
-0
lines changed

4 files changed

+128
-0
lines changed

api/package-lock.json

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@types/node": "^20.11.17",
3131
"@types/node-fetch": "^2.5.2",
3232
"@types/node-forge": "^1.3.1",
33+
"@types/nodemailer": "^6.4.17",
3334
"chai": "^4.2.0",
3435
"destroyable-server": "^1.0.1",
3536
"mocha": "^10.1.0",
@@ -51,6 +52,7 @@
5152
"node-cache": "^5.1.2",
5253
"node-fetch": "^2.6.1",
5354
"node-forge": "^1.3.1",
55+
"nodemailer": "^6.9.16",
5456
"php-serialize": "^2.1.0",
5557
"posthog-node": "^4.0.1",
5658
"tsx": "^4.1.2"

api/src/functions/contact-form.ts

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import nodemailer from 'nodemailer';
2+
3+
import { catchErrors, reportError } from '../errors';
4+
import { delay } from '@httptoolkit/util';
5+
import { getCorsResponseHeaders } from '../cors';
6+
7+
const {
8+
CONTACT_FORM_DESTINATION,
9+
SMTP_HOST,
10+
SMTP_PORT,
11+
SMTP_USERNAME,
12+
SMTP_PASSWORD
13+
} = process.env;
14+
15+
if (!CONTACT_FORM_DESTINATION) throw new Error('No contact form destination configured');
16+
17+
if (!SMTP_HOST) throw new Error('No SMTP host configured');
18+
if (!SMTP_PORT) throw new Error('No SMTP port configured');
19+
if (!SMTP_USERNAME) throw new Error('No SMTP user configured');
20+
if (!SMTP_PASSWORD) throw new Error('No SMTP password configured');
21+
22+
const mailer = nodemailer.createTransport({
23+
host: SMTP_HOST,
24+
port: Number(SMTP_PORT),
25+
secure: true,
26+
auth: {
27+
user: SMTP_USERNAME,
28+
pass: SMTP_PASSWORD
29+
}
30+
});
31+
32+
const THANK_YOU_PAGE = 'https://httptoolkit.com/contact-thank-you/'
33+
34+
export const handler = catchErrors(async (event) => {
35+
let headers = getCorsResponseHeaders(event);
36+
37+
if (event.httpMethod === 'OPTIONS') {
38+
return { statusCode: 200, headers, body: '' };
39+
} else if (event.httpMethod !== 'POST') {
40+
return { statusCode: 405, headers, body: '' };
41+
}
42+
43+
const formData = new URLSearchParams(event.body || '');
44+
const {
45+
name,
46+
email,
47+
message,
48+
phone: honeypot
49+
} = Object.fromEntries(formData);
50+
51+
if (honeypot) {
52+
// We can remove this later - just reporting each hit for now to check if it's working
53+
reportError('Contact form honeypot triggered', {
54+
extraMetadata: { name, email, message, honeypot }
55+
});
56+
57+
// Pretend it did actually work so they don't try again:
58+
await delay(1000);
59+
return {
60+
statusCode: 302,
61+
headers: {
62+
Location: THANK_YOU_PAGE
63+
},
64+
body: ''
65+
};
66+
}
67+
68+
const fields = [
69+
['Name', name],
70+
['Email', email],
71+
['Message', message]
72+
]
73+
74+
fields.forEach(([field, value]) => {
75+
if (!value) {
76+
return {
77+
statusCode: 400,
78+
headers,
79+
body: `${field} is required`
80+
};
81+
}
82+
});
83+
84+
await mailer.sendMail({
85+
from: 'Contact form <[email protected]>',
86+
to: CONTACT_FORM_DESTINATION,
87+
replyTo: email,
88+
subject: 'HTTP Toolkit contact form message',
89+
html: `<html><style>p { margin-bottom: 10px; }</style><body>
90+
${
91+
fields.map(([field, value]) => {
92+
return `<p><strong>${field}</strong>:<br/>${value}</p>`;
93+
}).join('')
94+
}</body></html>`
95+
});
96+
97+
return {
98+
statusCode: 302,
99+
headers: {
100+
Location: THANK_YOU_PAGE
101+
},
102+
body: ''
103+
};
104+
});

api/src/server.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ apiRouter.options('*', (req, res) => {
6565
'/auth/send-code',
6666
'/auth/login',
6767
'/auth/refresh-token',
68+
'/contact-form',
6869
'/update-team',
6970
'/update-team-size',
7071
'/cancel-subscription',
@@ -119,6 +120,8 @@ apiRouter.post('/auth/login',
119120
);
120121
apiRouter.post('/auth/refresh-token', rateLimit(RATE_LIMIT_PARAMS), lambdaWrapper('auth/refresh-token'));
121122

123+
apiRouter.post('/contact-form', lambdaWrapper('contact-form'));
124+
122125
apiRouter.post('/update-team', lambdaWrapper('update-team'));
123126
apiRouter.post('/update-team-size', lambdaWrapper('update-team-size'));
124127
apiRouter.post('/cancel-subscription', lambdaWrapper('cancel-subscription'));

0 commit comments

Comments
 (0)