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

@@ -0,0 +1,8 @@
import { getIcalResponse } from "../../../../../lib/ical-export";
export async function GET(
request: Request,
context: { params: { token: string; filename: string } }
) {
return getIcalResponse(request, context.params.token);
}

View File

@@ -1,66 +1,8 @@
import ical from "ical-generator";
import { NextResponse } from "next/server";
import { prisma } from "../../../../lib/prisma";
import { getIcalResponse } from "../../../../lib/ical-export";
export async function GET(
_request: Request,
request: Request,
context: { params: { token: string } }
) {
const view = await prisma.userView.findUnique({
where: { token: context.params.token },
include: {
items: { include: { event: true } },
categories: true,
exclusions: true,
user: true
}
});
if (!view) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
const calendar = ical({
name: `Vereinskalender - ${view.name}`,
timezone: "Europe/Berlin"
});
const excludedIds = new Set(view.exclusions.map((item) => item.eventId));
const explicitEvents = view.items
.map((item) => item.event)
.filter((event) => event.status === "APPROVED");
const categoryIds = view.categories.map((item) => item.categoryId);
const categoryEvents =
categoryIds.length > 0
? await prisma.event.findMany({
where: { categoryId: { in: categoryIds }, status: "APPROVED" }
})
: [];
const combined = [...explicitEvents, ...categoryEvents].filter(
(event, index, all) =>
all.findIndex((item) => item.id === event.id) === index &&
!excludedIds.has(event.id)
);
combined.forEach((event) => {
const start = event.startAt;
const end =
event.endAt || new Date(event.startAt.getTime() + 3 * 60 * 60 * 1000);
calendar.createEvent({
id: event.id,
summary: event.title,
description: event.description || undefined,
location: event.location || undefined,
start,
end
});
});
return new NextResponse(calendar.toString(), {
headers: {
"Content-Type": "text/calendar; charset=utf-8"
}
});
return getIcalResponse(request, context.params.token);
}

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",