From 8a75208d5b4c3709251ff3b74bb7af4119c2437d Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Tue, 14 Jul 2026 15:23:32 -0700 Subject: [PATCH] fix permissions loop for charts route --- src/app/api/websites/charts/route.ts | 10 +--- src/permissions/website.ts | 71 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 8 deletions(-) diff --git a/src/app/api/websites/charts/route.ts b/src/app/api/websites/charts/route.ts index b213e4fc4..76d3d1fdc 100644 --- a/src/app/api/websites/charts/route.ts +++ b/src/app/api/websites/charts/route.ts @@ -1,10 +1,10 @@ import { z } from 'zod'; import { fromZonedTime } from 'date-fns-tz'; import { parseDateRange } from '@/lib/date'; +import { canViewBatchWebsites } from '@/permissions/website'; import { parseRequest } from '@/lib/request'; import { json } from '@/lib/response'; import { timezoneParam } from '@/lib/schema'; -import { canViewWebsite } from '@/permissions'; import { getWebsiteListCharts } from '@/queries/sql'; const schema = z.object({ @@ -32,13 +32,7 @@ export async function GET(request: Request) { : fromZonedTime(defaultRange.startDate, timezone); const endDate = hasDateRange ? new Date(query.endAt) : fromZonedTime(defaultRange.endDate, timezone); - const websiteIds = ( - await Promise.all( - query.ids.map(async (websiteId: string) => - (await canViewWebsite(auth, websiteId)) ? websiteId : null, - ), - ) - ).filter(Boolean); + const websiteIds = await canViewBatchWebsites(auth, query.ids); const data = await getWebsiteListCharts(websiteIds, { startDate, diff --git a/src/permissions/website.ts b/src/permissions/website.ts index cf8e8d82d..e549b615e 100644 --- a/src/permissions/website.ts +++ b/src/permissions/website.ts @@ -1,6 +1,7 @@ import { hasPermission } from '@/lib/auth'; import { PERMISSIONS } from '@/lib/constants'; import { getEntity } from '@/lib/entity'; +import prisma from '@/lib/prisma'; import type { Auth } from '@/lib/types'; import { getTeamUser, getWebsite } from '@/queries/prisma'; @@ -39,6 +40,76 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri return false; } +export async function canViewBatchWebsites({ user, shareToken }: Auth, websiteIds: string[]) { + if (!websiteIds.length) { + return []; + } + + const requestedIds = Array.from(new Set(websiteIds)); + + if (user?.isAdmin) { + return requestedIds; + } + + const shareAllowedIds = new Set( + [ + shareToken?.websiteId, + shareToken?.pixelId, + shareToken?.linkId, + ...(shareToken?.websiteIds ?? []), + ...(shareToken?.pixelIds ?? []), + ...(shareToken?.linkIds ?? []), + ].filter((id): id is string => Boolean(id)), + ); + + if (!user) { + return requestedIds.filter(id => shareAllowedIds.has(id)); + } + + const websites = await prisma.client.website.findMany({ + where: { + id: { + in: requestedIds, + }, + }, + select: { + id: true, + userId: true, + teamId: true, + }, + }); + + const ownedIds = new Set( + websites.filter(website => website.userId === user.id).map(website => website.id), + ); + const teamIds = Array.from( + new Set(websites.map(website => website.teamId).filter((teamId): teamId is string => Boolean(teamId))), + ); + const teamUsers = teamIds.length + ? await prisma.client.teamUser.findMany({ + where: { + userId: user.id, + teamId: { + in: teamIds, + }, + }, + select: { + teamId: true, + }, + }) + : []; + const allowedTeamIds = new Set(teamUsers.map(teamUser => teamUser.teamId)); + const teamOwnedIds = new Set( + websites + .filter(website => website.teamId && allowedTeamIds.has(website.teamId)) + .map(website => website.id), + ); + + return requestedIds.filter( + id => shareAllowedIds.has(id) || ownedIds.has(id) || teamOwnedIds.has(id), + ); +} + export async function canViewAllWebsites({ user }: Auth) { return user?.isAdmin ?? false; }