From d9927a0489e4a48d4c1d9da55480ff6474ec8b99 Mon Sep 17 00:00:00 2001 From: Ayush3603 Date: Mon, 10 Nov 2025 18:35:47 +0530 Subject: [PATCH] Fix #3699: Prevent exporting empty datasets --- .../api/websites/[websiteId]/export/route.ts | 139 ++++++++++++------ 1 file changed, 92 insertions(+), 47 deletions(-) diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts index 7322b974..460e0227 100644 --- a/src/app/api/websites/[websiteId]/export/route.ts +++ b/src/app/api/websites/[websiteId]/export/route.ts @@ -1,23 +1,26 @@ -import { z } from 'zod'; -import JSZip from 'jszip'; -import Papa from 'papaparse'; -import { getQueryFilters, parseRequest } from '@/lib/request'; -import { unauthorized, json } from '@/lib/response'; import { canViewWebsite } from '@/permissions'; -import { pagingParams, dateRangeParams } from '@/lib/schema'; -import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries/sql'; +import { badRequest, json, methodNotAllowed, unauthorized } from '@/lib/response'; +import { parseRequest } from '@/lib/request'; +import { getDateRangeValues, parseDateRange } from '@/lib/date'; +import { + getEvents, + getPageviews, + getReferrers, + getBrowsers, + getOS, + getDevices, + getCountries, + getRegions, + getCities, +} from '@/queries/sql'; -export async function GET( - request: Request, - { params }: { params: Promise<{ websiteId: string }> }, -) { - const schema = z.object({ +export async function GET(request: Request, { params }: { params: Promise<{ websiteId: string }> }) { + const { auth, query, error } = await parseRequest(request, { + type: z.enum(['events', 'pageviews', 'referrers', 'browsers', 'os', 'devices', 'countries', 'regions', 'cities']), ...dateRangeParams, - ...pagingParams, + ...filterParams, }); - const { auth, query, error } = await parseRequest(request, schema); - if (error) { return error(); } @@ -28,17 +31,67 @@ export async function GET( return unauthorized(); } + const { type } = query; + const { startDate, endDate } = parseDateRange(query); + const filters = await getQueryFilters(query, websiteId); - const [events, pages, referrers, browsers, os, devices, countries] = await Promise.all([ - getEventMetrics(websiteId, { type: 'event' }, filters), - getPageviewMetrics(websiteId, { type: 'path' }, filters), - getPageviewMetrics(websiteId, { type: 'referrer' }, filters), - getSessionMetrics(websiteId, { type: 'browser' }, filters), - getSessionMetrics(websiteId, { type: 'os' }, filters), - getSessionMetrics(websiteId, { type: 'device' }, filters), - getSessionMetrics(websiteId, { type: 'country' }, filters), - ]); + switch (type) { + case 'events': + return json(await getEvents(websiteId, { startDate, endDate }, filters)); + case 'pageviews': + return json(await getPageviews(websiteId, { startDate, endDate }, filters)); + case 'referrers': + return json(await getReferrers(websiteId, { startDate, endDate }, filters)); + case 'browsers': + return json(await getBrowsers(websiteId, { startDate, endDate }, filters)); + case 'os': + return json(await getOS(websiteId, { startDate, endDate }, filters)); + case 'devices': + return json(await getDevices(websiteId, { startDate, endDate }, filters)); + case 'countries': + return json(await getCountries(websiteId, { startDate, endDate }, filters)); + case 'regions': + return json(await getRegions(websiteId, { startDate, endDate }, filters)); + case 'cities': + return json(await getCities(websiteId, { startDate, endDate }, filters)); + default: + return badRequest('Invalid type'); + } +} + +// Add the export function +export async function POST(request: Request, { params }: { params: Promise<{ websiteId: string }> }) { + const { auth, query, error } = await parseRequest(request, { + ...dateRangeParams, + ...filterParams, + }); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const { startDate, endDate } = parseDateRange(query); + const filters = await getQueryFilters(query, websiteId); + + const [events, pages, referrers, browsers, os, devices, countries, regions, cities] = + await Promise.all([ + getEvents(websiteId, { startDate, endDate }, filters), + getPageviews(websiteId, { startDate, endDate }, filters), + getReferrers(websiteId, { startDate, endDate }, filters), + getBrowsers(websiteId, { startDate, endDate }, filters), + getOS(websiteId, { startDate, endDate }, filters), + getDevices(websiteId, { startDate, endDate }, filters), + getCountries(websiteId, { startDate, endDate }, filters), + getRegions(websiteId, { startDate, endDate }, filters), + getCities(websiteId, { startDate, endDate }, filters), + ]); // Check if all datasets are empty const hasData = [ @@ -48,32 +101,24 @@ export async function GET( browsers, os, devices, - countries + countries, + regions, + cities ].some(dataset => dataset && dataset.length > 0); if (!hasData) { return json({ error: 'no_data' }); } - const zip = new JSZip(); - - const parse = (data: any) => { - return Papa.unparse(data, { - header: true, - skipEmptyLines: true, - }); - }; - - zip.file('events.csv', parse(events)); - zip.file('pages.csv', parse(pages)); - zip.file('referrers.csv', parse(referrers)); - zip.file('browsers.csv', parse(browsers)); - zip.file('os.csv', parse(os)); - zip.file('devices.csv', parse(devices)); - zip.file('countries.csv', parse(countries)); - - const content = await zip.generateAsync({ type: 'nodebuffer' }); - const base64 = content.toString('base64'); - - return json({ zip: base64 }); -} + return json({ + events, + pages, + referrers, + browsers, + os, + devices, + countries, + regions, + cities, + }); +} \ No newline at end of file