mirror of
https://github.com/lukevella/rallly.git
synced 2025-04-29 18:26:34 +02:00
🔒️ Harden rate limiting (#1598)
This commit is contained in:
parent
da10baaa98
commit
d71a2fb6b6
8 changed files with 38 additions and 35 deletions
|
@ -28,11 +28,16 @@ const handler = (req: NextRequest) => {
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
const ip =
|
||||||
|
process.env.NODE_ENV === "development" ? "127.0.0.1" : ipAddress(req);
|
||||||
|
|
||||||
|
const identifier =
|
||||||
|
session?.user?.id ?? req.headers.get("x-vercel-ja4-digest") ?? ip;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
locale,
|
locale,
|
||||||
ip:
|
identifier,
|
||||||
process.env.NODE_ENV === "development" ? "127.0.0.1" : ipAddress(req),
|
|
||||||
} satisfies TRPCContext;
|
} satisfies TRPCContext;
|
||||||
},
|
},
|
||||||
onError({ error }) {
|
onError({ error }) {
|
||||||
|
|
|
@ -11,5 +11,5 @@ type User = {
|
||||||
export type TRPCContext = {
|
export type TRPCContext = {
|
||||||
user?: User;
|
user?: User;
|
||||||
locale?: string;
|
locale?: string;
|
||||||
ip?: string;
|
identifier?: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -29,7 +29,7 @@ export const auth = router({
|
||||||
return { isRegistered: count > 0 };
|
return { isRegistered: count > 0 };
|
||||||
}),
|
}),
|
||||||
requestRegistration: publicProcedure
|
requestRegistration: publicProcedure
|
||||||
.use(createRateLimitMiddleware(5, "1 m"))
|
.use(createRateLimitMiddleware("request_registration", 5, "1 m"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
|
|
|
@ -130,7 +130,7 @@ export const polls = router({
|
||||||
|
|
||||||
// START LEGACY ROUTES
|
// START LEGACY ROUTES
|
||||||
create: possiblyPublicProcedure
|
create: possiblyPublicProcedure
|
||||||
.use(createRateLimitMiddleware(20, "1 h"))
|
.use(createRateLimitMiddleware("create_poll", 10, "1 h"))
|
||||||
.use(requireUserMiddleware)
|
.use(requireUserMiddleware)
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -233,7 +233,6 @@ export const polls = router({
|
||||||
return { id: poll.id };
|
return { id: poll.id };
|
||||||
}),
|
}),
|
||||||
update: possiblyPublicProcedure
|
update: possiblyPublicProcedure
|
||||||
.use(createRateLimitMiddleware(60, "1 h"))
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
urlId: z.string(),
|
urlId: z.string(),
|
||||||
|
@ -306,7 +305,6 @@ export const polls = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
delete: possiblyPublicProcedure
|
delete: possiblyPublicProcedure
|
||||||
.use(createRateLimitMiddleware(30, "1 h"))
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
urlId: z.string(),
|
urlId: z.string(),
|
||||||
|
|
|
@ -72,7 +72,7 @@ export const comments = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
add: publicProcedure
|
add: publicProcedure
|
||||||
.use(createRateLimitMiddleware(5, "1 m"))
|
.use(createRateLimitMiddleware("add_comment", 5, "1 m"))
|
||||||
.use(requireUserMiddleware)
|
.use(requireUserMiddleware)
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|
|
@ -105,7 +105,6 @@ export const participants = router({
|
||||||
return participants;
|
return participants;
|
||||||
}),
|
}),
|
||||||
delete: publicProcedure
|
delete: publicProcedure
|
||||||
.use(createRateLimitMiddleware(20, "1 m"))
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
participantId: z.string(),
|
participantId: z.string(),
|
||||||
|
@ -123,7 +122,7 @@ export const participants = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
add: publicProcedure
|
add: publicProcedure
|
||||||
.use(createRateLimitMiddleware(20, "1 m"))
|
.use(createRateLimitMiddleware("add_participant", 5, "1 m"))
|
||||||
.use(requireUserMiddleware)
|
.use(requireUserMiddleware)
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
@ -218,7 +217,6 @@ export const participants = router({
|
||||||
return participant;
|
return participant;
|
||||||
}),
|
}),
|
||||||
rename: publicProcedure
|
rename: publicProcedure
|
||||||
.use(createRateLimitMiddleware(20, "1 m"))
|
|
||||||
.input(z.object({ participantId: z.string(), newName: z.string() }))
|
.input(z.object({ participantId: z.string(), newName: z.string() }))
|
||||||
.mutation(async ({ input: { participantId, newName } }) => {
|
.mutation(async ({ input: { participantId, newName } }) => {
|
||||||
await prisma.participant.update({
|
await prisma.participant.update({
|
||||||
|
@ -232,7 +230,6 @@ export const participants = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
update: publicProcedure
|
update: publicProcedure
|
||||||
.use(createRateLimitMiddleware(20, "1 m"))
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
pollId: z.string(),
|
pollId: z.string(),
|
||||||
|
|
|
@ -38,22 +38,20 @@ export const user = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
delete: privateProcedure
|
delete: privateProcedure.mutation(async ({ ctx }) => {
|
||||||
.use(createRateLimitMiddleware(5, "1 h"))
|
if (ctx.user.isGuest) {
|
||||||
.mutation(async ({ ctx }) => {
|
throw new TRPCError({
|
||||||
if (ctx.user.isGuest) {
|
code: "BAD_REQUEST",
|
||||||
throw new TRPCError({
|
message: "Guest users cannot be deleted",
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message: "Guest users cannot be deleted",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.user.delete({
|
|
||||||
where: {
|
|
||||||
id: ctx.user.id,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}),
|
}
|
||||||
|
|
||||||
|
await prisma.user.delete({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
subscription: publicProcedure.query(
|
subscription: publicProcedure.query(
|
||||||
async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
|
async ({ ctx }): Promise<{ legacy?: boolean; active: boolean }> => {
|
||||||
if (!ctx.user || ctx.user.isGuest) {
|
if (!ctx.user || ctx.user.isGuest) {
|
||||||
|
@ -67,7 +65,6 @@ export const user = router({
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
changeName: privateProcedure
|
changeName: privateProcedure
|
||||||
.use(createRateLimitMiddleware(20, "1 h"))
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(100),
|
name: z.string().min(1).max(100),
|
||||||
|
@ -84,7 +81,6 @@ export const user = router({
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
updatePreferences: privateProcedure
|
updatePreferences: privateProcedure
|
||||||
.use(createRateLimitMiddleware(30, "1 h"))
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
locale: z.string().optional(),
|
locale: z.string().optional(),
|
||||||
|
@ -111,7 +107,7 @@ export const user = router({
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
requestEmailChange: privateProcedure
|
requestEmailChange: privateProcedure
|
||||||
.use(createRateLimitMiddleware(10, "1 h"))
|
.use(createRateLimitMiddleware("request_email_change", 10, "1 h"))
|
||||||
.input(z.object({ email: z.string().email() }))
|
.input(z.object({ email: z.string().email() }))
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const currentUser = await prisma.user.findUnique({
|
const currentUser = await prisma.user.findUnique({
|
||||||
|
@ -163,7 +159,7 @@ export const user = router({
|
||||||
return { success: true as const };
|
return { success: true as const };
|
||||||
}),
|
}),
|
||||||
getAvatarUploadUrl: privateProcedure
|
getAvatarUploadUrl: privateProcedure
|
||||||
.use(createRateLimitMiddleware(20, "1 h"))
|
.use(createRateLimitMiddleware("get_avatar_upload_url", 10, "1 h"))
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
fileType: z.enum(["image/jpeg", "image/png"]),
|
fileType: z.enum(["image/jpeg", "image/png"]),
|
||||||
|
@ -209,7 +205,6 @@ export const user = router({
|
||||||
}),
|
}),
|
||||||
updateAvatar: privateProcedure
|
updateAvatar: privateProcedure
|
||||||
.input(z.object({ imageKey: z.string().max(255) }))
|
.input(z.object({ imageKey: z.string().max(255) }))
|
||||||
.use(createRateLimitMiddleware(10, "1 h"))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
const oldImageKey = ctx.user.image;
|
const oldImageKey = ctx.user.image;
|
||||||
|
|
|
@ -90,6 +90,7 @@ export const proProcedure = privateProcedure.use(async ({ ctx, next }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
export const createRateLimitMiddleware = (
|
export const createRateLimitMiddleware = (
|
||||||
|
name: string,
|
||||||
requests: number,
|
requests: number,
|
||||||
duration: "1 m" | "1 h",
|
duration: "1 m" | "1 h",
|
||||||
) => {
|
) => {
|
||||||
|
@ -98,20 +99,27 @@ export const createRateLimitMiddleware = (
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.ip) {
|
if (!ctx.identifier) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "INTERNAL_SERVER_ERROR",
|
code: "INTERNAL_SERVER_ERROR",
|
||||||
message: "Failed to get client IP",
|
message: "Failed to get identifier",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const ratelimit = new Ratelimit({
|
const ratelimit = new Ratelimit({
|
||||||
redis: kv,
|
redis: kv,
|
||||||
limiter: Ratelimit.slidingWindow(requests, duration),
|
limiter: Ratelimit.slidingWindow(requests, duration),
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await ratelimit.limit(ctx.ip);
|
const res = await ratelimit.limit(`${name}:${ctx.identifier}`);
|
||||||
|
|
||||||
if (!res.success) {
|
if (!res.success) {
|
||||||
|
console.warn("Rate limit exceeded", {
|
||||||
|
identifier: ctx.identifier,
|
||||||
|
endpoint: name,
|
||||||
|
limit: requests,
|
||||||
|
duration,
|
||||||
|
});
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "TOO_MANY_REQUESTS",
|
code: "TOO_MANY_REQUESTS",
|
||||||
message: "Too many requests",
|
message: "Too many requests",
|
||||||
|
|
Loading…
Add table
Reference in a new issue