Add rate limit middleware for safe actions (#1836)

This commit is contained in:
Luke Vella 2025-07-16 18:55:48 +01:00 committed by GitHub
parent 502048488f
commit f8bdfdd067
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 40 additions and 14 deletions

View file

@ -1,24 +1,19 @@
"use server";
import { feedbackSchema } from "@/features/feedback/schema";
import { authActionClient } from "@/features/safe-action/server";
import {
authActionClient,
createRateLimitMiddleware,
} from "@/features/safe-action/server";
import { getEmailClient } from "@/utils/emails";
import { rateLimit } from "../rate-limit";
export const submitFeedbackAction = authActionClient
.metadata({
actionName: "submitFeedback",
actionName: "submit_feedback",
})
.use(createRateLimitMiddleware(5, "1 h"))
.inputSchema(feedbackSchema)
.action(async ({ ctx, parsedInput }) => {
const { success } = await rateLimit("submitFeedback", 3, "1h");
if (!success) {
return {
error: "Rate limit exceeded" as const,
};
}
try {
const { content } = parsedInput;
getEmailClient().sendEmail({

View file

@ -8,8 +8,8 @@ import { auth } from "@/next-auth";
import { isRateLimitEnabled } from "./constants";
type Unit = "ms" | "s" | "m" | "h" | "d";
type Duration = `${number} ${Unit}` | `${number}${Unit}`;
export type Unit = "ms" | "s" | "m" | "h" | "d";
export type Duration = `${number} ${Unit}` | `${number}${Unit}`;
async function getIPAddress() {
return (await headers()).get("x-forwarded-for");

View file

@ -32,6 +32,11 @@ export const useSafeAction: typeof useAction = (action, options) => {
defaultValue: "An internal server error occurred",
});
break;
case "TOO_MANY_REQUESTS":
translatedDescription = t("actionErrorTooManyRequests", {
defaultValue: "You are making too many requests",
});
break;
}
toast.error(translatedDescription);

View file

@ -6,12 +6,15 @@ import { revalidatePath } from "next/cache";
import { createMiddleware, createSafeActionClient } from "next-safe-action";
import z from "zod";
import { requireUserAbility } from "@/auth/queries";
import type { Duration } from "@/features/rate-limit";
import { rateLimit } from "@/features/rate-limit";
type ActionErrorCode =
| "UNAUTHORIZED"
| "NOT_FOUND"
| "FORBIDDEN"
| "INTERNAL_SERVER_ERROR";
| "INTERNAL_SERVER_ERROR"
| "TOO_MANY_REQUESTS";
export class ActionError extends Error {
code: ActionErrorCode;
@ -65,6 +68,29 @@ const posthogMiddleware = createMiddleware<{
return result;
});
export const createRateLimitMiddleware = (
requests: number,
duration: Duration,
) =>
createMiddleware<{
metadata: {
actionName: string;
};
}>().define<{
ctx: { user: { id: string } };
}>(async ({ next, metadata }) => {
const res = await rateLimit(metadata.actionName, requests, duration);
if (!res.success) {
throw new ActionError({
code: "TOO_MANY_REQUESTS",
message: "You are making too many requests.",
});
}
return next();
});
export const actionClient = createSafeActionClient({
defineMetadataSchema: () =>
z.object({