Aktueller Stand

This commit is contained in:
2026-01-15 23:18:42 +01:00
parent 46eae2a2a9
commit dcf45bac3d
32 changed files with 2625 additions and 395 deletions

View File

@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
import { parseICS } from "node-ical";
import { isAdminSession, requireSession } from "../../../../lib/auth-helpers";
import { prisma } from "../../../../lib/prisma";
import { checkRateLimit, getRateLimitConfig } from "../../../../lib/rate-limit";
import { getClientIp } from "../../../../lib/request";
const MAX_FILE_SIZE = 5 * 1024 * 1024;
@@ -11,6 +13,32 @@ const asText = (value: unknown) => {
return String(value).trim();
};
const parseGeo = (value: unknown) => {
if (!value) return null;
if (typeof value === "string") {
const cleaned = value.trim();
if (!cleaned) return null;
const parts = cleaned.split(/[;,]/).map((part) => part.trim());
if (parts.length >= 2) {
const lat = Number(parts[0]);
const lng = Number(parts[1]);
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
return { lat, lng };
}
}
return null;
}
if (typeof value === "object") {
const record = value as Record<string, unknown>;
const lat = Number(record.lat ?? record.latitude);
const lng = Number(record.lon ?? record.lng ?? record.longitude);
if (!Number.isNaN(lat) && !Number.isNaN(lng)) {
return { lat, lng };
}
}
return null;
};
export async function POST(request: Request) {
const { session } = await requireSession();
if (!session) {
@@ -20,6 +48,22 @@ export async function POST(request: Request) {
return NextResponse.json({ error: "Nur für Admins." }, { status: 403 });
}
const ip = getClientIp(request);
const email = session.user?.email || "unknown";
const rateKey = `icalimport:${email}:${ip}`;
const rateConfig = getRateLimitConfig("RATE_LIMIT_ICAL_IMPORT", 5);
const rate = await checkRateLimit({
key: rateKey,
limit: rateConfig.limit,
windowMs: rateConfig.windowMs
});
if (!rate.ok) {
return NextResponse.json(
{ error: "Zu viele Importe. Bitte später erneut versuchen." },
{ status: 429 }
);
}
const formData = await request.formData();
const file = formData.get("file");
const categoryId = asText(formData.get("categoryId"));
@@ -103,6 +147,7 @@ export async function POST(request: Request) {
: new Date(start.getTime() + 3 * 60 * 60 * 1000);
const location = asText(entry.location) || null;
const description = asText(entry.description) || null;
const geo = parseGeo(entry.geo);
const existing = await prisma.event.findFirst({
where: {
@@ -123,6 +168,8 @@ export async function POST(request: Request) {
title,
description,
location,
locationLat: geo ? geo.lat : null,
locationLng: geo ? geo.lng : null,
startAt: start,
endAt: end,
status: "APPROVED",