fix(security): rate-limit public invite and password-reset endpoints
- requestPasswordReset: rate-limited by email (authRateLimiter, 5/15 min) to prevent email bombing - resetPassword: rate-limited by token to add explicit brute-force defence - getInvite + acceptInvite: rate-limited by invite token (authRateLimiter) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import { SystemRole } from "@capakraken/db";
|
|||||||
import { createTRPCRouter, adminProcedure, publicProcedure } from "../trpc.js";
|
import { createTRPCRouter, adminProcedure, publicProcedure } from "../trpc.js";
|
||||||
import { getAppBaseUrl } from "../lib/app-base-url.js";
|
import { getAppBaseUrl } from "../lib/app-base-url.js";
|
||||||
import { sendEmail } from "../lib/email.js";
|
import { sendEmail } from "../lib/email.js";
|
||||||
|
import { authRateLimiter } from "../middleware/rate-limit.js";
|
||||||
|
|
||||||
const INVITE_TTL_MS = 72 * 60 * 60 * 1000; // 72 hours
|
const INVITE_TTL_MS = 72 * 60 * 60 * 1000; // 72 hours
|
||||||
|
|
||||||
@@ -85,6 +86,14 @@ export const inviteRouter = createTRPCRouter({
|
|||||||
getInvite: publicProcedure
|
getInvite: publicProcedure
|
||||||
.input(z.object({ token: z.string() }))
|
.input(z.object({ token: z.string() }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
|
const rl = await authRateLimiter(input.token);
|
||||||
|
if (!rl.allowed) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "TOO_MANY_REQUESTS",
|
||||||
|
message: "Too many attempts. Please wait before trying again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const invite = await ctx.db.inviteToken.findUnique({
|
const invite = await ctx.db.inviteToken.findUnique({
|
||||||
where: { token: input.token },
|
where: { token: input.token },
|
||||||
select: { email: true, role: true, expiresAt: true, usedAt: true },
|
select: { email: true, role: true, expiresAt: true, usedAt: true },
|
||||||
@@ -104,6 +113,14 @@ export const inviteRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const rl = await authRateLimiter(input.token);
|
||||||
|
if (!rl.allowed) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "TOO_MANY_REQUESTS",
|
||||||
|
message: "Too many attempts. Please wait before trying again.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const invite = await ctx.db.inviteToken.findUnique({
|
const invite = await ctx.db.inviteToken.findUnique({
|
||||||
where: { token: input.token },
|
where: { token: input.token },
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user