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; const asText = (value: unknown) => { if (value === null || value === undefined) return ""; if (typeof value === "string") return value.trim(); 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; 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) { return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); } if (!isAdminSession(session)) { 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")); if (!(file instanceof File)) { return NextResponse.json( { error: "Bitte eine iCal-Datei hochladen." }, { status: 400 } ); } if (!categoryId) { return NextResponse.json( { error: "Bitte eine Kategorie auswählen." }, { status: 400 } ); } const category = await prisma.category.findUnique({ where: { id: categoryId } }); if (!category) { return NextResponse.json( { error: "Kategorie nicht gefunden." }, { status: 404 } ); } if (file.size > MAX_FILE_SIZE) { return NextResponse.json( { error: "Datei ist zu groß (max. 5 MB)." }, { status: 400 } ); } let parsed: Record; try { const raw = await file.text(); parsed = parseICS(raw); } catch (err) { return NextResponse.json( { error: "iCal-Datei konnte nicht gelesen werden." }, { status: 400 } ); } const entries = Object.values(parsed).filter( (entry) => entry && entry.type === "VEVENT" ); if (entries.length === 0) { return NextResponse.json( { error: "Keine Termine in der iCal-Datei gefunden." }, { status: 400 } ); } let created = 0; let duplicates = 0; let skipped = 0; let recurringSkipped = 0; const creatorEmail = session.user?.email || ""; for (const entry of entries) { if (entry.rrule) { recurringSkipped += 1; continue; } const title = asText(entry.summary); const start = entry.start instanceof Date ? entry.start : null; if (!title || !start || Number.isNaN(start.getTime())) { skipped += 1; continue; } const end = entry.end instanceof Date && !Number.isNaN(entry.end.getTime()) ? entry.end : 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: { title, startAt: start, location, categoryId } }); if (existing) { duplicates += 1; continue; } await prisma.event.create({ data: { title, description, location, locationLat: geo ? geo.lat : null, locationLng: geo ? geo.lng : null, startAt: start, endAt: end, status: "APPROVED", createdBy: { connect: { email: creatorEmail } }, category: { connect: { id: categoryId } } } }); created += 1; } return NextResponse.json({ created, duplicates, skipped, recurringSkipped }); }