Skip to content

Commit

Permalink
use standard email/SMTP for contact form instead of airtable API
Browse files Browse the repository at this point in the history
  • Loading branch information
jakejarvis committed Feb 27, 2024
1 parent 2de3914 commit e624521
Show file tree
Hide file tree
Showing 11 changed files with 101 additions and 110 deletions.
7 changes: 3 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
# absolutely required:
NEXT_PUBLIC_BASE_URL=
DATABASE_URL=
GH_PUBLIC_TOKEN=
POSTGRES_URL=
POSTGRES_URL_NON_POOLING=

# optional (but not really):
AIRTABLE_API_KEY=
AIRTABLE_BASE=
HCAPTCHA_SECRET_KEY=
MAILGUN_SMTP_USER=
MAILGUN_SMTP_PASS=
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Licensed under CC-BY-4.0](https://img.shields.io/badge/license-CC--BY--4.0-fb7828?logo=creative-commons&logoColor=white)](LICENSE)
[![GitHub repo size](https://img.shields.io/github/repo-size/jakejarvis/jarv.is?color=009cdf&label=repo%20size&logo=git&logoColor=white)](https://github.com/jakejarvis/jarv.is)

My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Stitches](https://stitches.dev/), [Prisma](https://www.prisma.io/), [Vercel](https://vercel.com/), [Supabase](https://supabase.com/), [and more](https://jarv.is/humans.txt).
My humble abode on the World Wide Web, created and deployed using [Next.js](https://nextjs.org/), [Stitches](https://stitches.dev/), [Prisma](https://www.prisma.io/), [Vercel](https://vercel.com/), [PlanetScale](https://planetscale.com/), [and more](https://jarv.is/humans.txt).

I keep an ongoing list of [post ideas](https://github.com/jakejarvis/jarv.is/issues/1) and [coding to-dos](https://github.com/jakejarvis/jarv.is/issues/714) as issues in this repo. Outside contributions, improvements, and/or corrections are welcome too!

Expand Down
5 changes: 1 addition & 4 deletions lib/helpers/fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,6 @@
// https://nextjs.org/blog/next-9-1-7#new-built-in-polyfills-fetch-url-and-objectassign

// eslint-disable-next-line @typescript-eslint/no-explicit-any
const fetcher = async (input: RequestInfo, init?: RequestInit): Promise<any> => {
const res = await fetch(input, init);
return res.json();
};
const fetcher = <T = any>(...args: Parameters<typeof fetch>): Promise<T> => fetch(...args).then((res) => res.json());

export default fetcher;
10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"@hcaptcha/react-hcaptcha": "^1.10.1",
"@novnc/novnc": "1.4.0",
"@octokit/graphql": "^7.0.2",
"@octokit/graphql-schema": "^14.57.0",
"@octokit/graphql-schema": "^14.58.0",
"@prisma/client": "^5.10.2",
"@react-spring/web": "^9.7.3",
"@stitches/react": "1.3.1-1",
Expand All @@ -39,6 +39,7 @@
"next": "14.1.0",
"next-mdx-remote": "^4.4.1",
"next-seo": "^6.5.0",
"nodemailer": "^6.9.10",
"obj-str": "^1.1.0",
"p-map": "^7.0.1",
"p-memoize": "^7.1.1",
Expand Down Expand Up @@ -74,9 +75,10 @@
"@jakejarvis/eslint-config": "^3.1.0",
"@types/comma-number": "^2.1.2",
"@types/node": "^20.11.20",
"@types/nodemailer": "^6.4.14",
"@types/novnc__novnc": "^1.3.4",
"@types/prop-types": "^15.7.11",
"@types/react": "^18.2.58",
"@types/react": "^18.2.60",
"@types/react-dom": "^18.2.19",
"@types/react-is": "^18.2.4",
"@typescript-eslint/eslint-plugin": "^7.1.0",
Expand Down Expand Up @@ -111,9 +113,9 @@
"eslint"
]
},
"packageManager": "[email protected].3",
"packageManager": "[email protected].4",
"volta": {
"node": "20.11.1",
"pnpm": "8.15.3"
"pnpm": "8.15.4"
}
}
101 changes: 44 additions & 57 deletions pages/api/contact.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import { NextResponse } from "next/server";
import nodemailer from "nodemailer";
import queryString from "query-string";
import { siteDomain, hcaptchaSiteKey } from "../../lib/config";
import type { NextRequest } from "next/server";
import fetcher from "../../lib/helpers/fetcher";
import { siteDomain, authorName, authorEmail, hcaptchaSiteKey } from "../../lib/config";
import type { NextApiHandler } from "next";

// fallback to dummy secret for testing: https://docs.hcaptcha.com/#integration-testing-test-keys
const HCAPTCHA_SITE_KEY = hcaptchaSiteKey || "10000000-ffff-ffff-ffff-000000000001";
const HCAPTCHA_SECRET_KEY = process.env.HCAPTCHA_SECRET_KEY || "0x0000000000000000000000000000000000000000";
const HCAPTCHA_API_ENDPOINT = "https://hcaptcha.com/siteverify";

const { AIRTABLE_API_KEY, AIRTABLE_BASE } = process.env;
const AIRTABLE_API_ENDPOINT = "https://api.airtable.com/v0/";

export const config = {
runtime: "edge",
};

// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextRequest) => {
const handler: NextApiHandler = async (req, res) => {
// redirect GET requests to this endpoint to the contact form itself
if (req.method === "GET") {
return NextResponse.redirect(`${process.env.NEXT_PUBLIC_BASE_URL || `https://${siteDomain}`}/contact/`);
return res.redirect(`${process.env.NEXT_PUBLIC_BASE_URL || `https://${siteDomain}`}/contact/`);
}

// possible weirdness? https://github.com/orgs/vercel/discussions/78#discussioncomment-5089059
const data = await req.json();
const data = req.body;

// these are both backups to client-side validations just in case someone squeezes through without them. the codes
// are identical so they're caught in the same fashion.
Expand All @@ -36,61 +24,60 @@ export default async (req: NextRequest) => {
throw new Error("INVALID_CAPTCHA");
}

// sent directly to airtable
const airtableResult = await sendToAirtable({
Name: data.name,
Email: data.email,
Message: data.message,
});

// throw an internal error, not user's fault
if (airtableResult !== true) {
throw new Error("AIRTABLE_API_ERROR");
if (!(await sendMessage(data))) {
throw new Error("NODEMAILER_ERROR");
}

// disable caching on both ends. see:
// https://vercel.com/docs/concepts/functions/edge-functions/edge-caching
res.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");

// success! let the client know
return NextResponse.json(
{ success: true },
{
status: 201,
headers: {
// disable caching on both ends. see:
// https://vercel.com/docs/concepts/functions/edge-functions/edge-caching
"Cache-Control": "private, no-cache, no-store, must-revalidate",
},
}
);
return res.status(201).json({ success: true });
};

const validateCaptcha = async (formResponse: unknown): Promise<unknown> => {
const response = await fetch(HCAPTCHA_API_ENDPOINT, {
const response = await fetcher("https://hcaptcha.com/siteverify", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: queryString.stringify({
response: formResponse,
sitekey: HCAPTCHA_SITE_KEY,
secret: HCAPTCHA_SECRET_KEY,
// fallback to dummy secret for testing: https://docs.hcaptcha.com/#integration-testing-test-keys
sitekey: hcaptchaSiteKey || "10000000-ffff-ffff-ffff-000000000001",
secret: process.env.HCAPTCHA_SECRET_KEY || "0x0000000000000000000000000000000000000000",
}),
});

const result = await response.json();

return result.success;
return response?.success;
};

const sendToAirtable = async (data: unknown): Promise<boolean> => {
const response = await fetch(`${AIRTABLE_API_ENDPOINT}${AIRTABLE_BASE}/Messages`, {
method: "POST",
headers: {
Authorization: `Bearer ${AIRTABLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
fields: data,
}),
});
const sendMessage = async (data: Record<string, unknown>): Promise<boolean> => {
try {
const transporter = nodemailer.createTransport({
service: "mailgun",
auth: {
user: process.env.MAILGUN_SMTP_USER,
pass: process.env.MAILGUN_SMTP_PASS,
},
});

await transporter.sendMail({
from: `${data.name} <${process.env.MAILGUN_SMTP_USER}>`,
sender: `nodemailer <${process.env.MAILGUN_SMTP_USER}>`,
replyTo: `${data.name} <${data.email}>`,
to: `${authorName} <${authorEmail}>`,
subject: `[${siteDomain}] Contact Form Submission`,
text: `${data.message}`,
});
} catch (error) {
console.error(error);
return false;
}

return response.ok;
return true;
};

export default handler;
2 changes: 1 addition & 1 deletion pages/privacy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const Privacy = () => {

<p>
A very simple hit counter on each blog post tallies an aggregate number of pageviews (i.e.{" "}
<CodeInline>hits = hits + 1</CodeInline>) in a <Link href="https://supabase.com/">Supabase Postgres</Link>{" "}
<CodeInline>hits = hits + 1</CodeInline>) in a <Link href="https://planetscale.com/">PlanetScale</Link>{" "}
database. Individual views and identifying (or non-identifying) details are{" "}
<strong>never stored or logged</strong>.
</p>
Expand Down
Loading

1 comment on commit e624521

@vercel
Copy link

@vercel vercel bot commented on e624521 Feb 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

jarvis – ./

jarvis-jakejarvis.vercel.app
jarvis-git-main-jakejarvis.vercel.app
www.jarv.is
jarv.is

Please sign in to comment.