Files
anouma/app/(frontend)/calendar/[tokenFile]/route.ts
T

60 lines
2.1 KiB
TypeScript

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/<token>.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",
},
});
}