Update src/app/api/admin/guests/export/route.ts

This commit is contained in:
2026-01-18 10:05:40 +02:00
parent ad3eb4abbc
commit fd028f63df

View File

@@ -1,32 +1,12 @@
// src/app/api/admin/guests/export/route.ts
// Force dynamic rendering
export const dynamic = "force-dynamic";
export const dynamic = 'force-dynamic';
import { NextRequest, NextResponse } from "next/server";
import { verifyAdminSession } from "@/lib/auth";
import { verifyCsrfToken } from "@/lib/csrf";
import { prisma } from "@/lib/db";
import { getRequestId } from "@/lib/request-id";
import { logger } from "@/lib/logger";
type ExportFormat = "csv" | "json" | "xlsx";
type GuestExportRow = {
Name: string;
Surname: string;
Title: string;
Email: string;
Phone: string;
Status: string;
"Party Size": number;
Table: string;
"Dietary Restrictions": string;
Relationship: string;
"Relationship To": string;
"Invite Code": string;
"RSVP Date": string;
Notes: string;
};
import type { Prisma } from "@prisma/client";
export async function GET(request: NextRequest) {
const requestId = getRequestId(request);
@@ -38,63 +18,20 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
// Resolve admin identity for CSRF binding (robust across session shapes)
const adminId =
(session as any)?.adminId ||
(session as any)?.userId ||
(session as any)?.id ||
"admin";
const userType: "admin" = "admin";
// CSRF token (export is a privileged operation)
const csrfToken =
request.headers.get("x-csrf-token") || request.headers.get("csrf-token");
if (!csrfToken) {
return NextResponse.json({ error: "Missing CSRF token" }, { status: 403 });
}
const csrfValid = await verifyCsrfToken(csrfToken, String(adminId), userType);
if (!csrfValid) {
return NextResponse.json({ error: "Invalid CSRF token" }, { status: 403 });
}
const { searchParams } = new URL(request.url);
const idsParam = searchParams.get("ids");
const formatParam = (searchParams.get("format") || "csv").toLowerCase();
const format: ExportFormat =
formatParam === "json" || formatParam === "csv" || formatParam === "xlsx"
? (formatParam as ExportFormat)
: "csv";
const format = searchParams.get("format") || "csv";
// Get guest IDs to export
const guestIds = idsParam
? idsParam
.split(",")
.map((s) => s.trim())
.filter(Boolean)
: [];
const guestIds = idsParam ? idsParam.split(",") : [];
// Fetch guests (typed select; avoid implicit any)
// Fetch guests
const guests = await prisma.guest.findMany({
where: guestIds.length > 0 ? { id: { in: guestIds } } : undefined,
select: {
id: true,
name: true,
surname: true,
title: true,
email: true,
phone: true,
isAttending: true,
maxPax: true,
dietaryRestrictions: true,
relationship: true,
relationshipTo: true,
inviteCode: true,
rsvpTimestamp: true,
notes: true,
table: { select: { name: true } },
include: {
table: {
select: { name: true },
},
},
orderBy: { name: "asc" },
});
@@ -103,65 +40,73 @@ export async function GET(request: NextRequest) {
requestId,
format,
guestCount: guests.length,
hasIdsFilter: guestIds.length > 0,
});
const exportData: GuestExportRow[] = guests.map((guest) => ({
// Format data
type GuestWithTable = Prisma.GuestGetPayload<{
include: { table: { select: { name: true } } };
}>;
const exportData = guests.map((guest: GuestWithTable) => ({
Name: guest.name,
Surname: guest.surname || "",
Title: guest.title || "",
Email: guest.email || "",
Phone: guest.phone || "",
Status:
guest.isAttending === true
? "Confirmed"
: guest.isAttending === false
? "Declined"
: "Pending",
"Party Size": guest.maxPax ?? 1,
Status: guest.isAttending === true ? "Confirmed" : guest.isAttending === false ? "Declined" : "Pending",
"Party Size": guest.maxPax,
Table: guest.table?.name || "Unassigned",
"Dietary Restrictions": guest.dietaryRestrictions || "",
Relationship: guest.relationship || "",
"Relationship To": guest.relationshipTo || "",
"Invite Code": guest.inviteCode || "",
"RSVP Date": guest.rsvpTimestamp
? new Date(guest.rsvpTimestamp).toLocaleDateString("en-ZA")
: "",
"RSVP Date": guest.rsvpTimestamp ? new Date(guest.rsvpTimestamp).toLocaleDateString() : "",
Notes: guest.notes || "",
}));
if (format === "json") {
return new NextResponse(JSON.stringify(exportData, null, 2), {
status: 200,
headers: {
"Content-Type": "application/json; charset=utf-8",
"Content-Type": "application/json",
"Content-Disposition": `attachment; filename="guests-export.json"`,
"Cache-Control": "no-store",
},
});
}
// NOTE: For now, xlsx returns CSV to keep the build dependency-free.
// If you want true XLSX, add a library (e.g., exceljs) and generate a workbook.
const csv = toCSV(exportData);
if (format === "csv" || format === "xlsx") {
// Generate CSV
const headers = Object.keys(exportData[0] || {});
const csvRows = [
headers.join(","),
...exportData.map((row) =>
headers
.map((header) => {
const value = (row as any)[header] || "";
// Escape quotes and wrap in quotes if contains comma
if (typeof value === "string" && (value.includes(",") || value.includes('"'))) {
return `"${value.replace(/"/g, '""')}"`;
}
return value;
})
.join(",")
),
];
return new NextResponse(csv, {
status: 200,
headers: {
"Content-Type": "text/csv; charset=utf-8",
"Content-Disposition": `attachment; filename="guests-export.csv"`,
"Cache-Control": "no-store",
},
});
const csv = csvRows.join("\n");
return new NextResponse(csv, {
headers: {
"Content-Type": "text/csv",
"Content-Disposition": `attachment; filename="guests-export.csv"`,
},
});
}
return NextResponse.json({ error: "Invalid format" }, { status: 400 });
} catch (error) {
logger.error(
"Guest export error",
error instanceof Error ? error : new Error(String(error)),
{
requestId,
tags: { route: "admin/guests/export" },
}
);
logger.error("Guest export error", error instanceof Error ? error : new Error(String(error)), {
requestId,
tags: { route: "admin/guests/export" },
});
return NextResponse.json(
{
@@ -172,27 +117,3 @@ export async function GET(request: NextRequest) {
);
}
}
function toCSV(rows: GuestExportRow[]): string {
if (!rows || rows.length === 0) {
return "No data\n";
}
const headers = Object.keys(rows[0]) as Array<keyof GuestExportRow>;
const escape = (value: unknown) => {
const s = value === null || value === undefined ? "" : String(value);
if (/[",\n\r]/.test(s)) {
return `"${s.replace(/"/g, '""')}"`;
}
return s;
};
const lines = [
headers.map((h) => escape(h)).join(","),
...rows.map((row) => headers.map((h) => escape(row[h])).join(",")),
];
// Excel-friendly UTF-8 BOM
return "\uFEFF" + lines.join("\n") + "\n";
}