import { NextResponse } from "next/server"; import { getCMS } from "@/lib/payload/getPayload"; import { hashCalendarFeedToken } from "@/lib/calendar/feedToken"; import { buildICSCalendar } from "@/lib/calendar/ics"; import { getAdminFeedICSEvents } from "@/lib/calendar/adminFeed"; export const dynamic = "force-dynamic"; const TOKEN_SHAPE = /^[0-9a-f]{64}$/i; type Args = { params: Promise<{ tokenFile: string }> }; /** * GET /calendar/.ics — a private iCalendar feed, subscribable from * Apple Calendar, Google Calendar, Outlook, Thunderbird etc. The token * itself is the only credential (there's no login here), so it's treated * like a secret: looked up by hash only, never logged, and the response is * marked non-cacheable-by-proxies and non-indexable. */ export async function GET(_request: Request, { params }: Args) { const { tokenFile } = await params; if (!tokenFile.endsWith(".ics")) { return new NextResponse("Not found", { status: 404 }); } const token = tokenFile.slice(0, -".ics".length); if (!TOKEN_SHAPE.test(token)) { return new NextResponse("Not found", { status: 404 }); } const payload = await getCMS(); const hash = hashCalendarFeedToken(token); const { docs } = await payload.find({ collection: "users", where: { calendarFeedTokenHash: { equals: hash } }, limit: 1, }); // Section 15 / later extension: once normal user accounts also get a // personal feed, a matching lookup against "customers" would go here, // returning that customer's own bookings instead of the admin feed below. const owner = docs[0]; if (!owner) { return new NextResponse("Not found", { status: 404, headers: { "X-Robots-Tag": "noindex, nofollow" } }); } const events = await getAdminFeedICSEvents(payload); const ics = buildICSCalendar({ calendarName: "Persönlicher Kalender", events }); return new NextResponse(ics, { status: 200, headers: { "Content-Type": "text/calendar; charset=utf-8", "Content-Disposition": 'inline; filename="kalender.ics"', "X-Robots-Tag": "noindex, nofollow", "Cache-Control": "private, no-store", }, }); }