ultisuite-client/app/api/agenda/ical/route.web.ts
R3D347HR4Y d6d18f911b
Some checks failed
E2E / Playwright e2e (push) Has been cancelled
Lots of stuff and mobile app
2026-06-17 00:13:28 +02:00

61 lines
1.7 KiB
TypeScript

import { NextResponse } from "next/server"
const MAX_ICS_BYTES = 2 * 1024 * 1024
function isAllowedIcsUrl(raw: string): boolean {
try {
const url = new URL(raw)
if (url.protocol !== "http:" && url.protocol !== "https:") return false
const host = url.hostname.toLowerCase()
if (host === "localhost" || host === "127.0.0.1" || host === "::1") {
return false
}
return true
} catch {
return false
}
}
export async function GET(request: Request) {
const url = new URL(request.url).searchParams.get("url")?.trim()
if (!url || !isAllowedIcsUrl(url)) {
return NextResponse.json({ error: "invalid_url" }, { status: 400 })
}
try {
const res = await fetch(url, {
headers: { Accept: "text/calendar, text/plain, */*" },
redirect: "follow",
cache: "no-store",
})
if (!res.ok) {
return NextResponse.json({ error: "fetch_failed" }, { status: 502 })
}
const contentType = res.headers.get("content-type") ?? ""
if (
contentType &&
!contentType.includes("text/calendar") &&
!contentType.includes("text/plain") &&
!contentType.includes("application/octet-stream")
) {
return NextResponse.json({ error: "unsupported_content_type" }, { status: 415 })
}
const buffer = await res.arrayBuffer()
if (buffer.byteLength > MAX_ICS_BYTES) {
return NextResponse.json({ error: "payload_too_large" }, { status: 413 })
}
return new NextResponse(new TextDecoder("utf-8").decode(buffer), {
status: 200,
headers: {
"Content-Type": "text/calendar; charset=utf-8",
"Cache-Control": "private, max-age=300",
},
})
} catch {
return NextResponse.json({ error: "fetch_failed" }, { status: 502 })
}
}