diff --git a/.env.example b/.env.example index 7be94ad..8a5d4b8 100644 --- a/.env.example +++ b/.env.example @@ -3,6 +3,9 @@ BASEPATH= # Local dev only — CI sets this per workflow (prod vs develop). NEXT_PUBLIC_APP_URL=https://2026.pycon.co # Certificates registry (build-time only; do not use NEXT_PUBLIC_*). -# Prefer a full URL, or set the Drive file id and the download URL is derived. -# CERTIFICATES_JSON_URL=https://drive.google.com/uc?export=download&id=YOUR_FILE_ID +# Either works: raw file id, OR a Drive share/view/open URL (id is extracted). +# CERTIFICATES_DRIVE_FILE_ID=1kcQ-CP2o-b_Xq4w323U3h_nSrn6FOGCD +# CERTIFICATES_DRIVE_FILE_ID=https://drive.google.com/file/d/1kcQ-CP2o-b_Xq4w323U3h_nSrn6FOGCD/view?usp=sharing +# Optional direct download URL (Drive view/share links are auto-rewritten): +# CERTIFICATES_JSON_URL=https://drive.usercontent.google.com/download?id=FILE_ID&export=download CERTIFICATES_DRIVE_FILE_ID= \ No newline at end of file diff --git a/src/lib/certificate-registry.ts b/src/lib/certificate-registry.ts index 8e817aa..3f5267e 100644 --- a/src/lib/certificate-registry.ts +++ b/src/lib/certificate-registry.ts @@ -20,17 +20,55 @@ const ROLE_ALIASES: Record = { organizador: "organizer", }; +const DRIVE_FILE_ID_PATTERNS = [ + /\/file\/d\/([a-zA-Z0-9_-]+)/, + /[?&]id=([a-zA-Z0-9_-]+)/, + /^([a-zA-Z0-9_-]{20,})$/, +]; + let registryPromise: Promise> | null = null; +/** Accepts a raw Drive file id or any common Drive share/view/open/download URL. */ +export function extractDriveFileId(value: string): string | undefined { + const trimmed = value.trim().replace(/^["']|["']$/g, ""); + if (!trimmed) { + return undefined; + } + + for (const pattern of DRIVE_FILE_ID_PATTERNS) { + const match = trimmed.match(pattern); + if (match?.[1]) { + return match[1]; + } + } + + return undefined; +} + +function buildDriveDownloadUrl(fileId: string): string { + return `https://drive.usercontent.google.com/download?id=${encodeURIComponent(fileId)}&export=download`; +} + function getCertificatesJsonUrl(): string | undefined { const explicit = process.env.CERTIFICATES_JSON_URL?.trim(); if (explicit) { - return explicit; + const driveId = extractDriveFileId(explicit); + // Sharing/view links return HTML; always rewrite Drive links to the download endpoint. + if (driveId) { + return buildDriveDownloadUrl(driveId); + } + return explicit.replace(/^["']|["']$/g, ""); } - const fileId = process.env.CERTIFICATES_DRIVE_FILE_ID?.trim(); - if (fileId) { - return `https://drive.google.com/uc?export=download&id=${encodeURIComponent(fileId)}`; + const fileIdRaw = process.env.CERTIFICATES_DRIVE_FILE_ID?.trim(); + if (fileIdRaw) { + const fileId = extractDriveFileId(fileIdRaw); + if (!fileId) { + throw new Error( + "CERTIFICATES_DRIVE_FILE_ID must be a Drive file id or Drive URL containing one.", + ); + } + return buildDriveDownloadUrl(fileId); } return undefined; @@ -86,6 +124,15 @@ function parseCertificateList(payload: unknown): Map { return registry; } +function isHtmlBody(contentType: string, body: string): boolean { + const trimmed = body.trimStart(); + return ( + contentType.includes("text/html") || + trimmed.startsWith("> { const url = getCertificatesJsonUrl(); @@ -98,6 +145,12 @@ async function fetchCertificateRegistry(): Promise> { const response = await fetch(url, { // Build-time only; avoid Next Data Cache surprises across rebuilds. cache: "no-store", + redirect: "follow", + headers: { + // Some Drive edges return an HTML interstitial without a browser-like UA. + Accept: "application/json,text/plain,*/*", + "User-Agent": "PyConColombiaWebsiteBuild/1.0", + }, }); if (!response.ok) { @@ -109,13 +162,9 @@ async function fetchCertificateRegistry(): Promise> { const contentType = response.headers.get("content-type") ?? ""; const body = await response.text(); - if ( - contentType.includes("text/html") || - body.trimStart().startsWith("