feat(security): add Redis-backed rate limiting and CSP/security headers

Public go-live hardening: Redis-backed rate limiting on the previously
invite-gated public surface (explore browsing, post/comment creation,
invite-cookie routes) plus CSP, HSTS, and standard security headers on
every response, now that the invite gate no longer filters traffic.
This commit is contained in:
admin committed 2026-08-10 07:57:56 +02:00
1 parent ec9573f33a
commit f43541a4cc
11 files changed
+162 -8

No files matched your search

+11
View File
@@ -3,6 +3,8 @@ import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import { prisma } from "@/lib/prisma";
import { generateInviteCode, INVITE_CODES_PER_USER } from "@/lib/invite-codes";
import { checkRateLimit } from "@/lib/rate-limit";
import { getClientIp } from "@/lib/get-client-ip";
function resolveBase(request: NextRequest): string {
const proto = request.headers.get("x-forwarded-proto") ?? "https";
@@ -17,6 +19,15 @@ export async function GET(request: NextRequest) {
const { userId } = await auth();
const base = resolveBase(request);
const { success } = await checkRateLimit({
key: `auth.consume-invite:${getClientIp(request)}`,
limit: 20,
windowSeconds: 60,
});
if (!success) {
return NextResponse.redirect(new URL("/", base));
}
if (!userId) {
return NextResponse.redirect(new URL("/log-in", base));
}
+13 -1
View File
@@ -1,5 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { checkRateLimit } from "@/lib/rate-limit";
import { getClientIp } from "@/lib/get-client-ip";
function resolveBase(request: NextRequest): string {
const proto = request.headers.get("x-forwarded-proto") ?? "https";
@@ -11,9 +13,19 @@ function resolveBase(request: NextRequest): string {
}
export async function GET(request: NextRequest) {
const code = request.nextUrl.searchParams.get("code")?.trim().toUpperCase();
const base = resolveBase(request);
const { success } = await checkRateLimit({
key: `auth.set-invite:${getClientIp(request)}`,
limit: 20,
windowSeconds: 60,
});
if (!success) {
return NextResponse.redirect(new URL("/join?error=rate-limited", base));
}
const code = request.nextUrl.searchParams.get("code")?.trim().toUpperCase();
if (!code) {
return NextResponse.redirect(new URL("/join?error=missing", base));
}