Add rate limit on contact-us form using redis + move to app router (#4614)

## Problem solved

Short description of the bug fixed or feature added

<!-- start pr-codex -->

---

## PR-Codex overview
The focus of this PR is to implement rate limiting for a HubSpot API endpoint using Redis caching.

### Detailed summary
- Added `cacheGet` and `cacheSet` functions for Redis caching
- Implemented rate limiting logic based on IP address
- Added `rateLimiter` function to handle rate limiting
- Updated handler to use `rateLimiter` before processing requests

> ✨ Ask PR-Codex anything about this PR by commenting with `/codex {your question}`

<!-- end pr-codex -->
This commit is contained in:
MananTank
2024-09-16 16:32:48 +00:00
parent 2bf9fc598a
commit 6e5d89bf2a
4 changed files with 93 additions and 55 deletions
@@ -0,0 +1,78 @@
import { type NextRequest, NextResponse } from "next/server";
import invariant from "tiny-invariant";
import { cacheGet, cacheSet } from "../../../lib/redis";
import type { ContactFormPayload } from "./types";
// Note: This handler cannot use "edge" runtime because of Redis usage.
export const POST = async (req: NextRequest) => {
const rateLimitedResponse = await rateLimiter(req);
if (rateLimitedResponse) {
return rateLimitedResponse;
}
const requestBody = (await req.json()) as ContactFormPayload;
const { fields } = requestBody;
invariant(process.env.HUBSPOT_ACCESS_TOKEN, "missing HUBSPOT_ACCESS_TOKEN");
const response = await fetch(
"https://api.hsforms.com/submissions/v3/integration/secure/submit/23987964/38849262-3605-4eb2-883b-b4f1aa5ad845",
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
},
method: "POST",
body: JSON.stringify({ fields }),
},
);
if (!response.ok) {
const body = await response.json();
console.error("error", body);
}
return NextResponse.json(
{ status: response.statusText },
{
status: response.status,
},
);
};
async function rateLimiter(req: NextRequest) {
// Max 1 requests per minute
const rateLimitSeconds = 60;
const ipAddress =
req.headers.get("CF-Connecting-IP") ||
req.ip ||
req.headers.get("X-Forwarded-For");
if (!ipAddress) {
return NextResponse.json(
{
error: "Could not validate elligibility.",
},
{ status: 400 },
);
}
const cacheKey = `contact-us:${ipAddress}`;
const cacheValue = await cacheGet(cacheKey);
// if we have a cached value, return an error
if (cacheValue !== null) {
return NextResponse.json(
{
error: `Rate limit exceeded. Try again in ${rateLimitSeconds} seconds.`,
},
{ status: 429 },
);
}
// cache it for `rateLimitSeconds`
await cacheSet(cacheKey, "contact-us-used", rateLimitSeconds);
}
@@ -0,0 +1,6 @@
export type ContactFormPayload = {
fields: {
name: string;
value: string;
}[];
};
-48
View File
@@ -1,48 +0,0 @@
import { type NextRequest, NextResponse } from "next/server";
import invariant from "tiny-invariant";
export const config = {
runtime: "edge",
};
interface ContactFormPayload {
// biome-ignore lint/suspicious/noExplicitAny: FIXME
fields: any;
}
const handler = async (req: NextRequest) => {
if (req.method !== "POST") {
return NextResponse.json({ error: "invalid method" }, { status: 400 });
}
const requestBody = (await req.json()) as ContactFormPayload;
const { fields } = requestBody;
invariant(process.env.HUBSPOT_ACCESS_TOKEN, "missing HUBSPOT_ACCESS_TOKEN");
const response = await fetch(
"https://api.hsforms.com/submissions/v3/integration/secure/submit/23987964/38849262-3605-4eb2-883b-b4f1aa5ad845",
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${process.env.HUBSPOT_ACCESS_TOKEN}`,
},
method: "POST",
body: JSON.stringify({ fields }),
},
);
if (!response.ok) {
const body = await response.json();
console.error("error", body);
}
return NextResponse.json(
{ status: response.statusText },
{
status: response.status,
},
);
};
export default handler;
+9 -7
View File
@@ -23,6 +23,7 @@ import { useForm } from "react-hook-form";
import { BsFillLightningChargeFill } from "react-icons/bs";
import { Button, Card, Heading, Text } from "tw-components";
import type { ThirdwebNextPage } from "utils/types";
import type { ContactFormPayload } from "../app/api/contact-us/types";
interface FormSchema {
firstname: string;
@@ -139,11 +140,12 @@ const ContactUs: ThirdwebNextPage = () => {
gap={4}
as="form"
onSubmit={form.handleSubmit(async (data) => {
const fields = Object.keys(data).map((key) => ({
name: key,
// biome-ignore lint/suspicious/noExplicitAny: FIXME
value: (data as any)[key],
}));
const payload: ContactFormPayload = {
fields: Object.keys(data).map((key) => ({
name: key,
value: data[key as keyof FormSchema],
})),
};
setFormStatus("submitting");
@@ -154,9 +156,9 @@ const ContactUs: ThirdwebNextPage = () => {
});
try {
const response = await fetch("/api/hubspot", {
const response = await fetch("/api/contact-us", {
method: "POST",
body: JSON.stringify({ fields }),
body: JSON.stringify(payload),
});
if (!response.ok) {