Files
vereinskalender/app/api/ical/import/route.ts
2026-01-15 16:24:09 +01:00

144 lines
3.3 KiB
TypeScript

import { NextResponse } from "next/server";
import { parseICS } from "node-ical";
import { isAdminSession, requireSession } from "../../../../lib/auth-helpers";
import { prisma } from "../../../../lib/prisma";
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();
};
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 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<string, any>;
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 existing = await prisma.event.findFirst({
where: {
title,
startAt: start,
location,
categoryId
}
});
if (existing) {
duplicates += 1;
continue;
}
await prisma.event.create({
data: {
title,
description,
location,
startAt: start,
endAt: end,
status: "APPROVED",
createdBy: { connect: { email: creatorEmail } },
category: { connect: { id: categoryId } }
}
});
created += 1;
}
return NextResponse.json({
created,
duplicates,
skipped,
recurringSkipped
});
}