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

+10
View File
@@ -0,0 +1,10 @@
import "server-only";
import type { NextRequest } from "next/server";
export function getClientIp(request: NextRequest): string {
return (
request.headers.get("cf-connecting-ip") ??
request.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ??
"unknown"
);
}
+32
View File
@@ -0,0 +1,32 @@
import "server-only";
import { redis } from "@/lib/redis";
interface RateLimitOptions {
/** Unique key identifying the caller + the action being limited, e.g. "posts.create:user_123". */
key: string;
/** Max requests allowed within the window. */
limit: number;
windowSeconds: number;
}
interface RateLimitResult {
success: boolean;
}
/**
* Fixed-window rate limiter backed by Redis INCR/EXPIRE.
* Fails open (allows the request) if Redis is unreachable — a rate-limit
* outage must not take the whole app down with it.
*/
export async function checkRateLimit({ key, limit, windowSeconds }: RateLimitOptions): Promise<RateLimitResult> {
try {
const redisKey = `ratelimit:${key}`;
const count = await redis.incr(redisKey);
if (count === 1) {
await redis.expire(redisKey, windowSeconds);
}
return { success: count <= limit };
} catch {
return { success: true };
}
}