From 03adb6b7e1b5ea0139f3c4e3b961058d56cf26d5 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 4 Oct 2025 00:38:10 -0700 Subject: [PATCH] Added website check for cloud. --- .../websites/[websiteId]/WebsiteNav.tsx | 15 ++++- src/app/api/users/[userId]/usage/route.ts | 60 ------------------- src/app/api/websites/route.ts | 14 ++++- src/queries/prisma/website.ts | 8 +++ 4 files changed, 32 insertions(+), 65 deletions(-) delete mode 100644 src/app/api/users/[userId]/usage/route.ts diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 1c211eff..3696b786 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -1,6 +1,15 @@ import { Text } from '@umami/react-zen'; -import { Eye, User, Clock, Sheet, Tag, ChartPie, UserPlus } from '@/components/icons'; -import { Lightning, Path, Money, Compare, Target, Funnel, Magnet, Network } from '@/components/svg'; +import { + Eye, + User, + Clock, + Sheet, + Tag, + ChartPie, + UserPlus, + GitCompareArrows, +} from '@/components/icons'; +import { Lightning, Path, Money, Target, Funnel, Magnet, Network } from '@/components/svg'; import { useMessages, useNavigation } from '@/components/hooks'; import { SideMenu } from '@/components/common/SideMenu'; import { WebsiteSelect } from '@/components/input/WebsiteSelect'; @@ -47,7 +56,7 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { { id: 'compare', label: formatMessage(labels.compare), - icon: , + icon: , path: renderPath('/compare'), }, { diff --git a/src/app/api/users/[userId]/usage/route.ts b/src/app/api/users/[userId]/usage/route.ts deleted file mode 100644 index 677e0bd7..00000000 --- a/src/app/api/users/[userId]/usage/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { z } from 'zod'; -import { json, unauthorized } from '@/lib/response'; -import { getAllUserWebsitesIncludingTeamOwner } from '@/queries/prisma/website'; -import { getEventUsage } from '@/queries/sql/events/getEventUsage'; -import { getEventDataUsage } from '@/queries/sql/events/getEventDataUsage'; -import { parseRequest, getQueryFilters } from '@/lib/request'; - -export async function GET(request: Request, { params }: { params: Promise<{ userId: string }> }) { - const schema = z.object({ - startAt: z.coerce.number().int(), - endAt: z.coerce.number().int(), - }); - - const { auth, query, error } = await parseRequest(request, schema); - - if (error) { - return error(); - } - - if (!auth.user.isAdmin) { - return unauthorized(); - } - - const { userId } = await params; - const filters = await getQueryFilters(query); - - const websites = await getAllUserWebsitesIncludingTeamOwner(userId); - - const websiteIds = websites.map(a => a.id); - - const websiteEventUsage = await getEventUsage(websiteIds, filters); - const eventDataUsage = await getEventDataUsage(websiteIds, filters); - - const websiteUsage = websites.map(a => ({ - websiteId: a.id, - websiteName: a.name, - websiteEventUsage: websiteEventUsage.find(b => a.id === b.websiteId)?.count || 0, - eventDataUsage: eventDataUsage.find(b => a.id === b.websiteId)?.count || 0, - deletedAt: a.deletedAt, - })); - - const usage = websiteUsage.reduce( - (acc, cv) => { - acc.websiteEventUsage += cv.websiteEventUsage; - acc.eventDataUsage += cv.eventDataUsage; - - return acc; - }, - { websiteEventUsage: 0, eventDataUsage: 0 }, - ); - - const filteredWebsiteUsage = websiteUsage.filter( - a => !a.deletedAt && (a.websiteEventUsage > 0 || a.eventDataUsage > 0), - ); - - return json({ - ...usage, - websites: filteredWebsiteUsage, - }); -} diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index 821b6eaf..776f23b0 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -4,9 +4,11 @@ import { json, unauthorized } from '@/lib/response'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { pagingParams, searchParams } from '@/lib/schema'; -import { createWebsite } from '@/queries/prisma'; +import { createWebsite, getWebsiteCount } from '@/queries/prisma'; import { getAllUserWebsitesIncludingTeamOwner, getUserWebsites } from '@/queries/prisma/website'; +const CLOUD_WEBSITE_LIMIT = 3; + export async function GET(request: Request) { const schema = z.object({ ...pagingParams, @@ -36,7 +38,7 @@ export async function POST(request: Request) { name: z.string().max(100), domain: z.string().max(500), shareId: z.string().max(50).nullable().optional(), - teamId: z.string().nullable().optional(), + teamId: z.uuid().nullable().optional(), id: z.uuid().nullable().optional(), }); @@ -48,6 +50,14 @@ export async function POST(request: Request) { const { id, name, domain, shareId, teamId } = body; + if (process.env.CLOUD_MODE && !teamId && !auth.user.hasSubscription) { + const count = await getWebsiteCount(auth.user.id); + + if (count >= CLOUD_WEBSITE_LIMIT) { + return unauthorized({ message: 'Website limit reached.' }); + } + } + if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { return unauthorized(); } diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index b1a762b9..315dca03 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -203,3 +203,11 @@ export async function deleteWebsite(websiteId: string) { return data; }); } + +export async function getWebsiteCount(userId: string) { + return prisma.client.website.count({ + where: { + userId, + }, + }); +}