diff --git a/src/app/api/gdpr-export/[token]/route.ts b/src/app/api/gdpr-export/[token]/route.ts index 797b170..ff893f2 100644 --- a/src/app/api/gdpr-export/[token]/route.ts +++ b/src/app/api/gdpr-export/[token]/route.ts @@ -1,7 +1,9 @@ import "server-only"; -import { NextResponse } from "next/server"; +import { NextRequest, NextResponse } from "next/server"; import { prisma } from "@/lib/prisma"; import { downloadExport } from "@/lib/data-export-storage"; +import { checkRateLimit } from "@/lib/rate-limit"; +import { getClientIp } from "@/lib/get-client-ip"; /** * Public, unauthenticated download endpoint for GDPR Art. 15 exports — same @@ -11,11 +13,21 @@ import { downloadExport } from "@/lib/data-export-storage"; * clicks (Gitea #6 follow-up: the emailed link must read pawfeed.org). */ export async function GET( - _request: Request, + request: NextRequest, { params }: { params: Promise<{ token: string }> } ) { const { token } = await params; + const ip = getClientIp(request); + const { success } = await checkRateLimit({ + key: `gdpr-export:${ip}`, + limit: 20, + windowSeconds: 60, + }); + if (!success) { + return new NextResponse(null, { status: 429 }); + } + const exportToken = await prisma.dataExportToken.findUnique({ where: { token } }); if (!exportToken) { return NextResponse.json({ error: "Link ungültig." }, { status: 404 }); diff --git a/src/app/health-card/[token]/page.tsx b/src/app/health-card/[token]/page.tsx index 46f9371..d9715d4 100644 --- a/src/app/health-card/[token]/page.tsx +++ b/src/app/health-card/[token]/page.tsx @@ -1,6 +1,8 @@ import { notFound } from "next/navigation"; +import { headers } from "next/headers"; import { PawPrint, AlertTriangle } from "lucide-react"; import { prisma } from "@/lib/prisma"; +import { checkRateLimit } from "@/lib/rate-limit"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Badge } from "@/components/ui/badge"; import { Separator } from "@/components/ui/separator"; @@ -37,6 +39,38 @@ export default async function HealthCardPage({ params, searchParams }: PageProps const { print } = await searchParams; const autoPrint = print === "1"; + const headerList = await headers(); + const ip = + headerList.get("cf-connecting-ip") ?? + headerList.get("x-forwarded-for")?.split(",")[0]?.trim() ?? + "unknown"; + const { success } = await checkRateLimit({ + key: `health-card:${ip}`, + limit: 20, + windowSeconds: 60, + }); + if (!success) { + return ( +
+ Bitte versuche es in einer Minute erneut. +
+