Aktueller Stand
This commit is contained in:
8
app/api/ical/[token]/[filename]/route.ts
Normal file
8
app/api/ical/[token]/[filename]/route.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user