From 247e14646b843bb017a1fcaa0285d575985e6670 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Thu, 14 Aug 2025 23:48:11 -0700 Subject: [PATCH] Pixel/links development. New validations folder. More refactoring. --- .../(main)/admin/teams/AdminTeamsTable.tsx | 28 +- src/app/(main)/admin/users/UsersTable.tsx | 2 +- src/app/(main)/links/LinkDeleteButton.tsx | 2 +- src/app/(main)/links/LinkEditButton.tsx | 2 +- src/app/(main)/links/LinksTable.tsx | 23 +- src/app/(main)/links/[linkId]/route.ts | 0 src/app/(main)/pixels/PixelAddButton.tsx | 13 +- src/app/(main)/pixels/PixelAddForm.tsx | 62 ----- src/app/(main)/pixels/PixelDeleteButton.tsx | 55 ++++ src/app/(main)/pixels/PixelEditButton.tsx | 19 ++ src/app/(main)/pixels/PixelEditForm.tsx | 105 ++++++++ src/app/(main)/pixels/PixelsDataTable.tsx | 14 + src/app/(main)/pixels/PixelsPage.tsx | 12 +- src/app/(main)/pixels/PixelsTable.tsx | 45 ++++ src/app/(main)/pixels/[pixelId]/route.ts | 0 .../teams/[teamId]/TeamMemberEditButton.tsx | 2 +- .../teams/[teamId]/TeamMemberRemoveButton.tsx | 2 +- .../settings/websites/Websites.module.css | 11 - .../websites/[websiteId]/WebsiteData.tsx | 6 +- .../websites/[websiteId]/WebsiteSettings.tsx | 5 - .../{settings => }/teams/TeamsDataTable.tsx | 2 +- .../{settings => }/teams/TeamsTable.tsx | 0 .../websites/WebsiteAddButton.tsx | 0 .../websites/WebsiteAddForm.tsx | 0 .../websites/WebsitesDataTable.tsx | 0 .../websites/WebsitesHeader.tsx | 0 src/app/(main)/websites/WebsitesPage.tsx | 4 +- .../{settings => }/websites/WebsitesTable.tsx | 0 .../websites/[websiteId]/WebsiteNav.tsx | 19 ++ .../[websiteId]/cohorts/CohortAddButton.tsx | 26 ++ .../cohorts/CohortDeleteButton.tsx | 55 ++++ .../[websiteId]/cohorts/CohortEditButton.tsx | 34 +++ .../[websiteId]/cohorts/CohortEditForm.tsx | 100 +++++++ .../[websiteId]/cohorts/CohortsDataTable.tsx | 24 ++ .../[websiteId]/cohorts/CohortsPage.tsx | 16 ++ .../[websiteId]/cohorts/CohortsTable.tsx | 41 +++ .../websites/[websiteId]/cohorts/page.tsx | 12 + .../segments/SegmentDeleteButton.tsx | 2 +- .../segments/SegmentEditButton.tsx | 2 +- src/app/actions/getConfig.ts | 8 +- src/app/api/admin/teams/route.ts | 8 +- src/app/api/admin/users/route.ts | 4 +- src/app/api/admin/websites/route.ts | 4 +- src/app/api/auth/login/route.ts | 3 +- src/app/api/links/[linkId]/route.ts | 16 +- src/app/api/links/route.ts | 6 +- src/app/api/pixels/[pixelId]/route.ts | 84 ++++++ src/app/api/pixels/route.ts | 62 +++++ src/app/api/realtime/[websiteId]/route.ts | 2 +- src/app/api/reports/[reportId]/route.ts | 2 +- src/app/api/reports/attribution/route.ts | 2 +- src/app/api/reports/breakdown/route.ts | 2 +- src/app/api/reports/funnel/route.ts | 2 +- src/app/api/reports/goal/route.ts | 2 +- src/app/api/reports/journey/route.ts | 2 +- src/app/api/reports/retention/route.ts | 2 +- src/app/api/reports/revenue/route.ts | 2 +- src/app/api/reports/route.ts | 2 +- src/app/api/reports/utm/route.ts | 2 +- src/app/api/teams/[teamId]/links/route.ts | 6 +- .../[teamId]/pixels/{pixels.ts => route.ts} | 2 +- src/app/api/teams/[teamId]/route.ts | 2 +- .../teams/[teamId]/users/[userId]/route.ts | 2 +- src/app/api/teams/[teamId]/users/route.ts | 2 +- src/app/api/teams/[teamId]/websites/route.ts | 2 +- src/app/api/teams/join/route.ts | 2 +- src/app/api/teams/route.ts | 2 +- src/app/api/users/[userId]/route.ts | 2 +- src/app/api/users/route.ts | 3 +- .../api/websites/[websiteId]/active/route.ts | 6 +- .../websites/[websiteId]/daterange/route.ts | 6 +- .../[websiteId]/event-data/[eventId]/route.ts | 2 +- .../[websiteId]/event-data/events/route.ts | 2 +- .../[websiteId]/event-data/fields/route.ts | 2 +- .../event-data/properties/route.ts | 2 +- .../[websiteId]/event-data/stats/route.ts | 2 +- .../[websiteId]/event-data/values/route.ts | 2 +- .../api/websites/[websiteId]/events/route.ts | 2 +- .../[websiteId]/events/series/route.ts | 2 +- .../api/websites/[websiteId]/export/route.ts | 2 +- .../[websiteId]/metrics/expanded/route.ts | 2 +- .../api/websites/[websiteId]/metrics/route.ts | 2 +- .../websites/[websiteId]/pageviews/route.ts | 2 +- .../api/websites/[websiteId]/reports/route.ts | 2 +- .../api/websites/[websiteId]/reset/route.ts | 2 +- src/app/api/websites/[websiteId]/route.ts | 2 +- .../[websiteId]/segments/[segmentId]/route.ts | 2 +- .../websites/[websiteId]/segments/route.ts | 2 +- .../session-data/properties/route.ts | 2 +- .../[websiteId]/session-data/values/route.ts | 2 +- .../sessions/[sessionId]/activity/route.ts | 2 +- .../sessions/[sessionId]/properties/route.ts | 2 +- .../[websiteId]/sessions/[sessionId]/route.ts | 2 +- .../websites/[websiteId]/sessions/route.ts | 2 +- .../[websiteId]/sessions/stats/route.ts | 2 +- .../[websiteId]/sessions/weekly/route.ts | 2 +- .../api/websites/[websiteId]/stats/route.ts | 2 +- .../websites/[websiteId]/transfer/route.ts | 2 +- .../api/websites/[websiteId]/values/route.ts | 2 +- src/app/api/websites/route.ts | 2 +- src/components/common/DateDistance.tsx | 11 +- src/components/common/ExternalLink.tsx | 18 ++ src/components/hooks/index.ts | 42 ++- .../hooks/queries/useActiveUsersQuery.ts | 2 +- .../hooks/queries/useEventDataEventsQuery.ts | 2 +- .../queries/useEventDataPropertiesQuery.ts | 2 +- .../hooks/queries/useEventDataQuery.ts | 6 +- .../hooks/queries/useEventDataValuesQuery.ts | 2 +- src/components/hooks/queries/useLinksQuery.ts | 6 +- .../hooks/queries/usePixelsQuery.ts | 14 +- .../hooks/queries/useReportsQuery.ts | 2 +- .../queries/useSessionDataPropertiesQuery.ts | 2 +- .../queries/useSessionDataValuesQuery.ts | 2 +- src/components/hooks/queries/useTeamQuery.ts | 2 +- src/components/hooks/queries/useUserQuery.ts | 2 +- .../hooks/queries/useUserWebsitesQuery.ts | 17 +- .../hooks/queries/useWebsiteCohortQuery.ts | 21 ++ .../hooks/queries/useWebsiteCohortsQuery.ts | 25 ++ .../hooks/queries/useWebsiteEventsQuery.ts | 2 +- .../queries/useWebsiteEventsSeriesQuery.ts | 2 +- .../hooks/queries/useWebsiteQuery.ts | 2 +- .../hooks/queries/useWebsiteSegmentQuery.ts | 2 +- .../hooks/queries/useWebsiteSegmentsQuery.ts | 2 +- ...PageParameters.ts => usePageParameters.ts} | 0 src/components/input/ActionButton.tsx | 13 +- src/components/messages.ts | 1 + src/lib/auth.ts | 251 +----------------- src/queries/prisma/link.ts | 10 +- src/queries/prisma/website.ts | 5 +- src/validations/index.ts | 6 + src/validations/link.ts | 64 +++++ src/validations/pixel.ts | 64 +++++ src/validations/report.ts | 27 ++ src/validations/team.ts | 86 ++++++ src/validations/user.ts | 29 ++ src/validations/website.ts | 110 ++++++++ 136 files changed, 1395 insertions(+), 516 deletions(-) create mode 100644 src/app/(main)/links/[linkId]/route.ts delete mode 100644 src/app/(main)/pixels/PixelAddForm.tsx create mode 100644 src/app/(main)/pixels/PixelDeleteButton.tsx create mode 100644 src/app/(main)/pixels/PixelEditButton.tsx create mode 100644 src/app/(main)/pixels/PixelEditForm.tsx create mode 100644 src/app/(main)/pixels/PixelsDataTable.tsx create mode 100644 src/app/(main)/pixels/PixelsTable.tsx create mode 100644 src/app/(main)/pixels/[pixelId]/route.ts delete mode 100644 src/app/(main)/settings/websites/Websites.module.css rename src/app/(main)/{settings => }/teams/TeamsDataTable.tsx (89%) rename src/app/(main)/{settings => }/teams/TeamsTable.tsx (100%) rename src/app/(main)/{settings => }/websites/WebsiteAddButton.tsx (100%) rename src/app/(main)/{settings => }/websites/WebsiteAddForm.tsx (100%) rename src/app/(main)/{settings => }/websites/WebsitesDataTable.tsx (100%) rename src/app/(main)/{settings => }/websites/WebsitesHeader.tsx (100%) rename src/app/(main)/{settings => }/websites/WebsitesTable.tsx (100%) create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/CohortAddButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/CohortEditButton.tsx create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/CohortEditForm.tsx create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/CohortsDataTable.tsx create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/CohortsPage.tsx create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx create mode 100644 src/app/(main)/websites/[websiteId]/cohorts/page.tsx rename src/app/api/teams/[teamId]/pixels/{pixels.ts => route.ts} (94%) create mode 100644 src/components/common/ExternalLink.tsx create mode 100644 src/components/hooks/queries/useWebsiteCohortQuery.ts create mode 100644 src/components/hooks/queries/useWebsiteCohortsQuery.ts rename src/components/hooks/{uesPageParameters.ts => usePageParameters.ts} (100%) create mode 100644 src/validations/index.ts create mode 100644 src/validations/link.ts create mode 100644 src/validations/pixel.ts create mode 100644 src/validations/report.ts create mode 100644 src/validations/team.ts create mode 100644 src/validations/user.ts create mode 100644 src/validations/website.ts diff --git a/src/app/(main)/admin/teams/AdminTeamsTable.tsx b/src/app/(main)/admin/teams/AdminTeamsTable.tsx index bacb7de8..d7c99a26 100644 --- a/src/app/(main)/admin/teams/AdminTeamsTable.tsx +++ b/src/app/(main)/admin/teams/AdminTeamsTable.tsx @@ -20,23 +20,25 @@ export function AdminTeamsTable({ return ( <> - + {(row: any) => {row.name}} - - {(row: any) => row?._count?.teamUser} + + {(row: any) => row?._count?.members} - - {(row: any) => row?._count?.website} + + {(row: any) => row?._count?.websites} - - {(row: any) => ( - - - {row?.teamUser?.[0]?.user?.username} - - - )} + + {(row: any) => { + const name = row?.members?.[0]?.user?.username; + + return ( + + {name} + + ); + }} {(row: any) => } diff --git a/src/app/(main)/admin/users/UsersTable.tsx b/src/app/(main)/admin/users/UsersTable.tsx index a78e66f2..6b365691 100644 --- a/src/app/(main)/admin/users/UsersTable.tsx +++ b/src/app/(main)/admin/users/UsersTable.tsx @@ -33,7 +33,7 @@ export function UsersTable({ } - {(row: any) => row._count.websiteUser} + {(row: any) => row._count.websites} {(row: any) => } diff --git a/src/app/(main)/links/LinkDeleteButton.tsx b/src/app/(main)/links/LinkDeleteButton.tsx index 015af001..58d9c1f6 100644 --- a/src/app/(main)/links/LinkDeleteButton.tsx +++ b/src/app/(main)/links/LinkDeleteButton.tsx @@ -34,7 +34,7 @@ export function LinkDeleteButton({ }; return ( - }> + }> {({ close }) => ( }> + }> {({ close }) => { return ; diff --git a/src/app/(main)/links/LinksTable.tsx b/src/app/(main)/links/LinksTable.tsx index e06e2537..0b41342e 100644 --- a/src/app/(main)/links/LinksTable.tsx +++ b/src/app/(main)/links/LinksTable.tsx @@ -1,13 +1,16 @@ import { DataTable, DataColumn, Row } from '@umami/react-zen'; -import { useMessages, useNavigation } from '@/components/hooks'; +import { useConfig, useMessages, useNavigation } from '@/components/hooks'; import { Empty } from '@/components/common/Empty'; import { DateDistance } from '@/components/common/DateDistance'; +import { ExternalLink } from '@/components/common/ExternalLink'; import { LinkEditButton } from './LinkEditButton'; import { LinkDeleteButton } from './LinkDeleteButton'; export function LinksTable({ data = [] }) { const { formatMessage, labels } = useMessages(); const { websiteId } = useNavigation(); + const { linksUrl } = useConfig(); + const hostUrl = linksUrl || `${window.location.origin}/x`; if (data.length === 0) { return ; @@ -16,14 +19,22 @@ export function LinksTable({ data = [] }) { return ( - - + + {({ slug }: any) => { + const url = `${hostUrl}/${slug}`; + return {url}; + }} + + + {({ url }: any) => { + return {url}; + }} + + {(row: any) => } - {(row: any) => { - const { id, name } = row; - + {({ id, name }: any) => { return ( diff --git a/src/app/(main)/links/[linkId]/route.ts b/src/app/(main)/links/[linkId]/route.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/(main)/pixels/PixelAddButton.tsx b/src/app/(main)/pixels/PixelAddButton.tsx index a74a455e..0958ff0e 100644 --- a/src/app/(main)/pixels/PixelAddButton.tsx +++ b/src/app/(main)/pixels/PixelAddButton.tsx @@ -1,17 +1,16 @@ -import { useMessages, useModified, useNavigation } from '@/components/hooks'; +import { useMessages, useModified } from '@/components/hooks'; import { Button, Icon, Modal, Dialog, DialogTrigger, Text, useToast } from '@umami/react-zen'; import { Plus } from '@/components/icons'; -import { PixelAddForm } from './PixelAddForm'; +import { PixelEditForm } from './PixelEditForm'; -export function PixelAddButton() { +export function PixelAddButton({ teamId }: { teamId?: string }) { const { formatMessage, labels, messages } = useMessages(); const { toast } = useToast(); const { touch } = useModified(); - const { teamId } = useNavigation(); const handleSave = async () => { toast(formatMessage(messages.saved)); - touch('boards'); + touch('pixels'); }; return ( @@ -23,8 +22,8 @@ export function PixelAddButton() { {formatMessage(labels.addPixel)} - - {({ close }) => } + + {({ close }) => } diff --git a/src/app/(main)/pixels/PixelAddForm.tsx b/src/app/(main)/pixels/PixelAddForm.tsx deleted file mode 100644 index 56ce44b1..00000000 --- a/src/app/(main)/pixels/PixelAddForm.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Form, FormField, FormSubmitButton, Row, TextField, Button } from '@umami/react-zen'; -import { useApi } from '@/components/hooks'; -import { DOMAIN_REGEX } from '@/lib/constants'; -import { useMessages } from '@/components/hooks'; - -export function PixelAddForm({ - teamId, - onSave, - onClose, -}: { - teamId?: string; - onSave?: () => void; - onClose?: () => void; -}) { - const { formatMessage, labels, messages } = useMessages(); - const { post, useMutation } = useApi(); - const { mutate, error, isPending } = useMutation({ - mutationFn: (data: any) => post('/pixels', { ...data, teamId }), - }); - - const handleSubmit = async (data: any) => { - mutate(data, { - onSuccess: async () => { - onSave?.(); - onClose?.(); - }, - }); - }; - - return ( -
- - - - - - - - - {onClose && ( - - )} - - {formatMessage(labels.save)} - - -
- ); -} diff --git a/src/app/(main)/pixels/PixelDeleteButton.tsx b/src/app/(main)/pixels/PixelDeleteButton.tsx new file mode 100644 index 00000000..3e5bf7fc --- /dev/null +++ b/src/app/(main)/pixels/PixelDeleteButton.tsx @@ -0,0 +1,55 @@ +import { Dialog } from '@umami/react-zen'; +import { ActionButton } from '@/components/input/ActionButton'; +import { Trash } from '@/components/icons'; +import { ConfirmationForm } from '@/components/common/ConfirmationForm'; +import { messages } from '@/components/messages'; +import { useApi, useMessages, useModified } from '@/components/hooks'; + +export function PixelDeleteButton({ + pixelId, + websiteId, + name, + onSave, +}: { + pixelId: string; + websiteId: string; + name: string; + onSave?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { del, useMutation } = useApi(); + const { mutate, isPending, error } = useMutation({ + mutationFn: () => del(`/websites/${websiteId}/pixels/${pixelId}`), + }); + const { touch } = useModified(); + + const handleConfirm = (close: () => void) => { + mutate(null, { + onSuccess: () => { + touch('pixels'); + onSave?.(); + close(); + }, + }); + }; + + return ( + }> + + {({ close }) => ( + + )} + + + ); +} diff --git a/src/app/(main)/pixels/PixelEditButton.tsx b/src/app/(main)/pixels/PixelEditButton.tsx new file mode 100644 index 00000000..289e4a90 --- /dev/null +++ b/src/app/(main)/pixels/PixelEditButton.tsx @@ -0,0 +1,19 @@ +import { ActionButton } from '@/components/input/ActionButton'; +import { Edit } from '@/components/icons'; +import { Dialog } from '@umami/react-zen'; +import { PixelEditForm } from './PixelEditForm'; +import { useMessages } from '@/components/hooks'; + +export function PixelEditButton({ pixelId }: { pixelId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + }> + + {({ close }) => { + return ; + }} + + + ); +} diff --git a/src/app/(main)/pixels/PixelEditForm.tsx b/src/app/(main)/pixels/PixelEditForm.tsx new file mode 100644 index 00000000..a622d018 --- /dev/null +++ b/src/app/(main)/pixels/PixelEditForm.tsx @@ -0,0 +1,105 @@ +import { + Form, + FormField, + FormSubmitButton, + Row, + TextField, + Button, + Text, + Label, + Column, + Icon, + Loading, +} from '@umami/react-zen'; +import { useConfig, usePixelQuery } from '@/components/hooks'; +import { useMessages } from '@/components/hooks'; +import { Refresh } from '@/components/icons'; +import { getRandomChars } from '@/lib/crypto'; +import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery'; + +const generateId = () => getRandomChars(9); + +export function PixelEditForm({ + pixelId, + teamId, + onSave, + onClose, +}: { + pixelId?: string; + teamId?: string; + onSave?: () => void; + onClose?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { mutate, error, isPending } = useUpdateQuery('/pixels', { id: pixelId, teamId }); + const { pixelDomain } = useConfig(); + const { data, isLoading } = usePixelQuery(pixelId); + + const handleSubmit = async (data: any) => { + mutate(data, { + onSuccess: async () => { + onSave?.(); + onClose?.(); + }, + }); + }; + + if (pixelId && !isLoading) { + return ; + } + + return ( +
+ {({ setValue }) => { + return ( + <> + + + + + + + + {pixelDomain || window.location.origin}/ + + + + + + + + + {onClose && ( + + )} + {formatMessage(labels.save)} + + + ); + }} +
+ ); +} diff --git a/src/app/(main)/pixels/PixelsDataTable.tsx b/src/app/(main)/pixels/PixelsDataTable.tsx new file mode 100644 index 00000000..6a1eb982 --- /dev/null +++ b/src/app/(main)/pixels/PixelsDataTable.tsx @@ -0,0 +1,14 @@ +import { usePixelsQuery, useNavigation } from '@/components/hooks'; +import { PixelsTable } from './PixelsTable'; +import { DataGrid } from '@/components/common/DataGrid'; + +export function PixelsDataTable() { + const { teamId } = useNavigation(); + const query = usePixelsQuery({ teamId }); + + return ( + + {({ data }) => } + + ); +} diff --git a/src/app/(main)/pixels/PixelsPage.tsx b/src/app/(main)/pixels/PixelsPage.tsx index a7a9e13a..58ccb7d1 100644 --- a/src/app/(main)/pixels/PixelsPage.tsx +++ b/src/app/(main)/pixels/PixelsPage.tsx @@ -3,17 +3,23 @@ import { PageBody } from '@/components/common/PageBody'; import { Column } from '@umami/react-zen'; import { PageHeader } from '@/components/common/PageHeader'; import { PixelAddButton } from './PixelAddButton'; -import { useMessages } from '@/components/hooks'; +import { useMessages, useNavigation } from '@/components/hooks'; +import { PixelsDataTable } from './PixelsDataTable'; +import { Panel } from '@/components/common/Panel'; export function PixelsPage() { const { formatMessage, labels } = useMessages(); + const { teamId } = useNavigation(); return ( - + - + + + + ); diff --git a/src/app/(main)/pixels/PixelsTable.tsx b/src/app/(main)/pixels/PixelsTable.tsx new file mode 100644 index 00000000..fff7f2f7 --- /dev/null +++ b/src/app/(main)/pixels/PixelsTable.tsx @@ -0,0 +1,45 @@ +import { DataTable, DataColumn, Row } from '@umami/react-zen'; +import { useConfig, useMessages, useNavigation } from '@/components/hooks'; +import { Empty } from '@/components/common/Empty'; +import { DateDistance } from '@/components/common/DateDistance'; +import { PixelEditButton } from './PixelEditButton'; +import { PixelDeleteButton } from './PixelDeleteButton'; +import Link from 'next/link'; + +export function PixelsTable({ data = [] }) { + const { formatMessage, labels } = useMessages(); + const { websiteId } = useNavigation(); + const { pixelsUrl } = useConfig(); + const defaultUrl = `${window.location.origin}/p`; + + if (data.length === 0) { + return ; + } + + return ( + + + + {({ slug }: any) => { + const url = `${pixelsUrl || defaultUrl}/${slug}`; + return {url}; + }} + + + {(row: any) => } + + + {(row: any) => { + const { id, name } = row; + + return ( + + + + + ); + }} + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/route.ts b/src/app/(main)/pixels/[pixelId]/route.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx b/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx index a9216439..6db7e147 100644 --- a/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx +++ b/src/app/(main)/settings/teams/[teamId]/TeamMemberEditButton.tsx @@ -26,7 +26,7 @@ export function TeamMemberEditButton({ }; return ( - }> + }> {({ close }) => ( }> + }> {({ close }) => ( - teamUser.find( + teams?.data?.filter(({ members }) => + members.find( ({ role, userId }) => [ROLES.teamOwner, ROLES.teamManager].includes(role) && userId === user.id, ), @@ -34,7 +34,7 @@ export function WebsiteData({ websiteId, onSave }: { websiteId: string; onSave?: (teamId && !!teams?.data ?.find(({ id }) => id === teamId) - ?.teamUser.find(({ role, userId }) => role === ROLES.teamOwner && userId === user.id)); + ?.members.find(({ role, userId }) => role === ROLES.teamOwner && userId === user.id)); const handleSave = () => { touch('websites'); diff --git a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx index fedcc245..baf59452 100644 --- a/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx +++ b/src/app/(main)/settings/websites/[websiteId]/WebsiteSettings.tsx @@ -6,7 +6,6 @@ import { WebsiteShareForm } from './WebsiteShareForm'; import { WebsiteTrackingCode } from './WebsiteTrackingCode'; import { WebsiteData } from './WebsiteData'; import { WebsiteEditForm } from './WebsiteEditForm'; -import { SegmentsDataTable } from '@/app/(main)/websites/[websiteId]/segments/SegmentsDataTable'; export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal?: boolean }) { const website = useContext(WebsiteContext); @@ -18,7 +17,6 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal {formatMessage(labels.details)} {formatMessage(labels.trackingCode)} {formatMessage(labels.shareUrl)} - {formatMessage(labels.segments)} {formatMessage(labels.manage)} @@ -30,9 +28,6 @@ export function WebsiteSettings({ websiteId }: { websiteId: string; openExternal - - - diff --git a/src/app/(main)/settings/teams/TeamsDataTable.tsx b/src/app/(main)/teams/TeamsDataTable.tsx similarity index 89% rename from src/app/(main)/settings/teams/TeamsDataTable.tsx rename to src/app/(main)/teams/TeamsDataTable.tsx index 5c6aa887..7700627c 100644 --- a/src/app/(main)/settings/teams/TeamsDataTable.tsx +++ b/src/app/(main)/teams/TeamsDataTable.tsx @@ -1,5 +1,5 @@ import { DataGrid } from '@/components/common/DataGrid'; -import { TeamsTable } from '@/app/(main)/settings/teams/TeamsTable'; +import { TeamsTable } from './TeamsTable'; import { useLoginQuery, useUserTeamsQuery } from '@/components/hooks'; import { ReactNode } from 'react'; diff --git a/src/app/(main)/settings/teams/TeamsTable.tsx b/src/app/(main)/teams/TeamsTable.tsx similarity index 100% rename from src/app/(main)/settings/teams/TeamsTable.tsx rename to src/app/(main)/teams/TeamsTable.tsx diff --git a/src/app/(main)/settings/websites/WebsiteAddButton.tsx b/src/app/(main)/websites/WebsiteAddButton.tsx similarity index 100% rename from src/app/(main)/settings/websites/WebsiteAddButton.tsx rename to src/app/(main)/websites/WebsiteAddButton.tsx diff --git a/src/app/(main)/settings/websites/WebsiteAddForm.tsx b/src/app/(main)/websites/WebsiteAddForm.tsx similarity index 100% rename from src/app/(main)/settings/websites/WebsiteAddForm.tsx rename to src/app/(main)/websites/WebsiteAddForm.tsx diff --git a/src/app/(main)/settings/websites/WebsitesDataTable.tsx b/src/app/(main)/websites/WebsitesDataTable.tsx similarity index 100% rename from src/app/(main)/settings/websites/WebsitesDataTable.tsx rename to src/app/(main)/websites/WebsitesDataTable.tsx diff --git a/src/app/(main)/settings/websites/WebsitesHeader.tsx b/src/app/(main)/websites/WebsitesHeader.tsx similarity index 100% rename from src/app/(main)/settings/websites/WebsitesHeader.tsx rename to src/app/(main)/websites/WebsitesHeader.tsx diff --git a/src/app/(main)/websites/WebsitesPage.tsx b/src/app/(main)/websites/WebsitesPage.tsx index d07192dc..db563147 100644 --- a/src/app/(main)/websites/WebsitesPage.tsx +++ b/src/app/(main)/websites/WebsitesPage.tsx @@ -1,9 +1,9 @@ 'use client'; -import { WebsitesDataTable } from '@/app/(main)/settings/websites/WebsitesDataTable'; +import { WebsitesDataTable } from './WebsitesDataTable'; +import { WebsiteAddButton } from './WebsiteAddButton'; import { useMessages, useNavigation } from '@/components/hooks'; import { Column } from '@umami/react-zen'; import { PageHeader } from '@/components/common/PageHeader'; -import { WebsiteAddButton } from '@/app/(main)/settings/websites/WebsiteAddButton'; import { Panel } from '@/components/common/Panel'; import { PageBody } from '@/components/common/PageBody'; diff --git a/src/app/(main)/settings/websites/WebsitesTable.tsx b/src/app/(main)/websites/WebsitesTable.tsx similarity index 100% rename from src/app/(main)/settings/websites/WebsitesTable.tsx rename to src/app/(main)/websites/WebsitesTable.tsx diff --git a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx index 799ba76c..74a9479b 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteNav.tsx @@ -11,6 +11,8 @@ import { Tag, Money, Network, + ChartPie, + UserPlus, } from '@/components/icons'; import { useMessages, useNavigation } from '@/components/hooks'; import { SideMenu } from '@/components/common/SideMenu'; @@ -87,6 +89,23 @@ export function WebsiteNav({ websiteId }: { websiteId: string }) { }, ], }, + { + label: formatMessage(labels.audience), + items: [ + { + id: 'segments', + label: formatMessage(labels.segments), + icon: , + path: renderPath('/segments'), + }, + { + id: 'cohorts', + label: formatMessage(labels.cohorts), + icon: , + path: renderPath('/cohorts'), + }, + ], + }, { label: formatMessage(labels.growth), items: [ diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortAddButton.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortAddButton.tsx new file mode 100644 index 00000000..4ddc3c55 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortAddButton.tsx @@ -0,0 +1,26 @@ +import { Button, DialogTrigger, Modal, Text, Icon, Dialog } from '@umami/react-zen'; +import { useMessages } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { CohortEditForm } from './CohortEditForm'; + +export function CohortAddButton({ websiteId }: { websiteId: string }) { + const { formatMessage, labels } = useMessages(); + + return ( + + + + + {({ close }) => { + return ; + }} + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx new file mode 100644 index 00000000..c7e6d679 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton.tsx @@ -0,0 +1,55 @@ +import { Dialog } from '@umami/react-zen'; +import { ActionButton } from '@/components/input/ActionButton'; +import { Trash } from '@/components/icons'; +import { ConfirmationForm } from '@/components/common/ConfirmationForm'; +import { messages } from '@/components/messages'; +import { useApi, useMessages, useModified } from '@/components/hooks'; + +export function CohortDeleteButton({ + cohortId, + websiteId, + name, + onSave, +}: { + cohortId: string; + websiteId: string; + name: string; + onSave?: () => void; +}) { + const { formatMessage, labels } = useMessages(); + const { del, useMutation } = useApi(); + const { mutate, isPending, error } = useMutation({ + mutationFn: () => del(`/websites/${websiteId}/cohorts/${cohortId}`), + }); + const { touch } = useModified(); + + const handleConfirm = (close: () => void) => { + mutate(null, { + onSuccess: () => { + touch('cohorts'); + onSave?.(); + close(); + }, + }); + }; + + return ( + }> + + {({ close }) => ( + + )} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortEditButton.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditButton.tsx new file mode 100644 index 00000000..51ac8505 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditButton.tsx @@ -0,0 +1,34 @@ +import { ActionButton } from '@/components/input/ActionButton'; +import { Edit } from '@/components/icons'; +import { Dialog } from '@umami/react-zen'; +import { CohortEditForm } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditForm'; +import { useMessages } from '@/components/hooks'; + +export function CohortEditButton({ + cohortId, + websiteId, + filters, +}: { + cohortId: string; + websiteId: string; + filters: any[]; +}) { + const { formatMessage, labels } = useMessages(); + + return ( + }> + + {({ close }) => { + return ( + + ); + }} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortEditForm.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditForm.tsx new file mode 100644 index 00000000..6b16a470 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortEditForm.tsx @@ -0,0 +1,100 @@ +import { + Button, + Form, + FormButtons, + FormField, + FormSubmitButton, + TextField, + Label, + Loading, +} from '@umami/react-zen'; +import { subMonths, endOfDay } from 'date-fns'; +import { FieldFilters } from '@/components/input/FieldFilters'; +import { useState } from 'react'; +import { useApi, useMessages, useModified, useWebsiteCohortQuery } from '@/components/hooks'; +import { filtersArrayToObject } from '@/lib/params'; + +export function CohortEditForm({ + cohortId, + websiteId, + filters = [], + showFilters = true, + onSave, + onClose, +}: { + cohortId?: string; + websiteId: string; + filters?: any[]; + showFilters?: boolean; + onSave?: () => void; + onClose?: () => void; +}) { + const { data } = useWebsiteCohortQuery(websiteId, cohortId); + const { formatMessage, labels } = useMessages(); + const [currentFilters, setCurrentFilters] = useState(filters); + const { touch } = useModified(); + const startDate = subMonths(endOfDay(new Date()), 6); + const endDate = endOfDay(new Date()); + + const { post, useMutation } = useApi(); + const { mutate, error, isPending } = useMutation({ + mutationFn: (data: any) => + post(`/websites/${websiteId}/cohorts${cohortId ? `/${cohortId}` : ''}`, { + ...data, + type: 'cohort', + }), + }); + + const handleSubmit = async (data: any) => { + mutate( + { ...data, parameters: filtersArrayToObject(currentFilters) }, + { + onSuccess: async () => { + touch('cohorts'); + onSave?.(); + onClose?.(); + }, + }, + ); + }; + + if (cohortId && !data) { + return ; + } + + return ( +
+ + + + {showFilters && ( + <> + + + + )} + + + + {formatMessage(labels.save)} + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsDataTable.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsDataTable.tsx new file mode 100644 index 00000000..5944d415 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsDataTable.tsx @@ -0,0 +1,24 @@ +import { CohortAddButton } from './CohortAddButton'; +import { useWebsiteCohortsQuery } from '@/components/hooks'; +import { CohortsTable } from './CohortsTable'; +import { DataGrid } from '@/components/common/DataGrid'; + +export function CohortsDataTable({ websiteId }: { websiteId?: string }) { + const query = useWebsiteCohortsQuery(websiteId, { type: 'cohort' }); + + const renderActions = () => { + return ; + }; + + return ( + + {({ data }) => } + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsPage.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsPage.tsx new file mode 100644 index 00000000..211e2526 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsPage.tsx @@ -0,0 +1,16 @@ +'use client'; +import { Column } from '@umami/react-zen'; +import { CohortsDataTable } from './CohortsDataTable'; +import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteControls'; +import { Panel } from '@/components/common/Panel'; + +export function CohortsPage({ websiteId }) { + return ( + + + + + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx new file mode 100644 index 00000000..578c4fc6 --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/CohortsTable.tsx @@ -0,0 +1,41 @@ +import { DataTable, DataColumn, Row } from '@umami/react-zen'; +import { useMessages, useNavigation } from '@/components/hooks'; +import { Empty } from '@/components/common/Empty'; +import { DateDistance } from '@/components/common/DateDistance'; +import { filtersObjectToArray } from '@/lib/params'; +import { CohortEditButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortEditButton'; +import { CohortDeleteButton } from '@/app/(main)/websites/[websiteId]/cohorts/CohortDeleteButton'; + +export function CohortsTable({ data = [] }) { + const { formatMessage, labels } = useMessages(); + const { websiteId } = useNavigation(); + + if (data.length === 0) { + return ; + } + + return ( + + + + {(row: any) => } + + + {(row: any) => { + const { id, name, parameters } = row; + + return ( + + + + + ); + }} + + + ); +} diff --git a/src/app/(main)/websites/[websiteId]/cohorts/page.tsx b/src/app/(main)/websites/[websiteId]/cohorts/page.tsx new file mode 100644 index 00000000..a9519c2c --- /dev/null +++ b/src/app/(main)/websites/[websiteId]/cohorts/page.tsx @@ -0,0 +1,12 @@ +import { Metadata } from 'next'; +import { CohortsPage } from './CohortsPage'; + +export default async function ({ params }: { params: Promise<{ websiteId: string }> }) { + const { websiteId } = await params; + + return ; +} + +export const metadata: Metadata = { + title: 'Cohorts', +}; diff --git a/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx index fc86e78f..3561af85 100644 --- a/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx +++ b/src/app/(main)/websites/[websiteId]/segments/SegmentDeleteButton.tsx @@ -34,7 +34,7 @@ export function SegmentDeleteButton({ }; return ( - }> + }> {({ close }) => ( }> + }> {({ close }) => { return ( diff --git a/src/app/actions/getConfig.ts b/src/app/actions/getConfig.ts index 44823379..9c26354a 100644 --- a/src/app/actions/getConfig.ts +++ b/src/app/actions/getConfig.ts @@ -6,8 +6,8 @@ export type Config = { telemetryDisabled: boolean; trackerScriptName?: string; updatesDisabled: boolean; - linkDomain?: string; - pixelDomain?: string; + linksUrl?: string; + pixelsUrl?: string; }; export async function getConfig(): Promise { @@ -17,7 +17,7 @@ export async function getConfig(): Promise { telemetryDisabled: !!process.env.DISABLE_TELEMETRY, trackerScriptName: process.env.TRACKER_SCRIPT_NAME, updatesDisabled: !!process.env.DISABLE_UPDATES, - linkDomain: process.env.LINK_DOMAIN, - pixelDomain: process.env.PIXEL_DOMAIN, + linksUrl: process.env.LINKS_URL, + pixelsUrl: process.env.PIXELS_URL, }; } diff --git a/src/app/api/admin/teams/route.ts b/src/app/api/admin/teams/route.ts index 6b162050..a47930b8 100644 --- a/src/app/api/admin/teams/route.ts +++ b/src/app/api/admin/teams/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { pagingParams, searchParams } from '@/lib/schema'; -import { canViewAllTeams } from '@/lib/auth'; +import { canViewAllTeams } from '@/validations'; import { getTeams } from '@/queries/prisma/team'; export async function GET(request: Request) { @@ -26,11 +26,11 @@ export async function GET(request: Request) { include: { _count: { select: { - teamUser: true, - website: true, + members: true, + websites: true, }, }, - teamUser: { + members: { select: { user: { omit: { diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index d0d3fcbd..ec074e01 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { pagingParams, searchParams } from '@/lib/schema'; -import { canViewUsers } from '@/lib/auth'; +import { canViewUsers } from '@/validations'; import { getUsers } from '@/queries/prisma/user'; export async function GET(request: Request) { @@ -26,7 +26,7 @@ export async function GET(request: Request) { include: { _count: { select: { - websiteUser: { + websites: { where: { deletedAt: null }, }, }, diff --git a/src/app/api/admin/websites/route.ts b/src/app/api/admin/websites/route.ts index 2b82fbe8..fda98a47 100644 --- a/src/app/api/admin/websites/route.ts +++ b/src/app/api/admin/websites/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { pagingParams, searchParams } from '@/lib/schema'; -import { canViewAllWebsites } from '@/lib/auth'; +import { canViewAllWebsites } from '@/validations'; import { getWebsites } from '@/queries/prisma/website'; import { ROLES } from '@/lib/constants'; @@ -39,7 +39,7 @@ export async function GET(request: Request) { deletedAt: null, }, include: { - teamUser: { + members: { where: { role: ROLES.teamOwner, }, diff --git a/src/app/api/auth/login/route.ts b/src/app/api/auth/login/route.ts index 042f2143..f70cfc8e 100644 --- a/src/app/api/auth/login/route.ts +++ b/src/app/api/auth/login/route.ts @@ -1,11 +1,10 @@ import { z } from 'zod'; -import { checkPassword } from '@/lib/auth'; import { createSecureToken } from '@/lib/jwt'; import redis from '@/lib/redis'; import { getUserByUsername } from '@/queries'; import { json, unauthorized } from '@/lib/response'; import { parseRequest } from '@/lib/request'; -import { saveAuth } from '@/lib/auth'; +import { saveAuth, checkPassword } from '@/lib/auth'; import { secret } from '@/lib/crypto'; import { ROLES } from '@/lib/constants'; diff --git a/src/app/api/links/[linkId]/route.ts b/src/app/api/links/[linkId]/route.ts index 4f8763b5..daeaff9b 100644 --- a/src/app/api/links/[linkId]/route.ts +++ b/src/app/api/links/[linkId]/route.ts @@ -1,9 +1,9 @@ import { z } from 'zod'; -import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/lib/auth'; +import { canUpdateLink, canDeleteLink, canViewLink } from '@/validations'; import { SHARE_ID_REGEX } from '@/lib/constants'; import { parseRequest } from '@/lib/request'; import { ok, json, unauthorized, serverError } from '@/lib/response'; -import { deleteWebsite, getWebsite, updateWebsite } from '@/queries'; +import { deleteLink, getLink, updateLink } from '@/queries'; export async function GET( request: Request, @@ -17,11 +17,11 @@ export async function GET( const { websiteId } = await params; - if (!(await canViewWebsite(auth, websiteId))) { + if (!(await canViewLink(auth, websiteId))) { return unauthorized(); } - const website = await getWebsite(websiteId); + const website = await getLink(websiteId); return json(website); } @@ -45,12 +45,12 @@ export async function POST( const { websiteId } = await params; const { name, domain, shareId } = body; - if (!(await canUpdateWebsite(auth, websiteId))) { + if (!(await canUpdateLink(auth, websiteId))) { return unauthorized(); } try { - const website = await updateWebsite(websiteId, { name, domain, shareId }); + const website = await updateLink(websiteId, { name, domain, shareId }); return Response.json(website); } catch (e: any) { @@ -74,11 +74,11 @@ export async function DELETE( const { websiteId } = await params; - if (!(await canDeleteWebsite(auth, websiteId))) { + if (!(await canDeleteLink(auth, websiteId))) { return unauthorized(); } - await deleteWebsite(websiteId); + await deleteLink(websiteId); return ok(); } diff --git a/src/app/api/links/route.ts b/src/app/api/links/route.ts index 8dfd7655..2ab561b6 100644 --- a/src/app/api/links/route.ts +++ b/src/app/api/links/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { canCreateTeamWebsite, canCreateWebsite } from '@/lib/auth'; +import { canCreateTeamWebsite, canCreateWebsite } from '@/validations'; import { json, unauthorized } from '@/lib/response'; import { uuid } from '@/lib/crypto'; import { getQueryFilters, parseRequest } from '@/lib/request'; @@ -20,9 +20,9 @@ export async function GET(request: Request) { const filters = await getQueryFilters(query); - const result = await getUserLinks(auth.user.id, filters); + const links = await getUserLinks(auth.user.id, filters); - return json(result); + return json(links); } export async function POST(request: Request) { diff --git a/src/app/api/pixels/[pixelId]/route.ts b/src/app/api/pixels/[pixelId]/route.ts index e69de29b..e57a90b0 100644 --- a/src/app/api/pixels/[pixelId]/route.ts +++ b/src/app/api/pixels/[pixelId]/route.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; +import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/validations'; +import { SHARE_ID_REGEX } from '@/lib/constants'; +import { parseRequest } from '@/lib/request'; +import { ok, json, unauthorized, serverError } from '@/lib/response'; +import { deleteWebsite, getWebsite, updateWebsite } from '@/queries'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canViewWebsite(auth, websiteId))) { + return unauthorized(); + } + + const website = await getWebsite(websiteId); + + return json(website); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const schema = z.object({ + name: z.string().optional(), + domain: z.string().optional(), + shareId: z.string().regex(SHARE_ID_REGEX).nullable().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { websiteId } = await params; + const { name, domain, shareId } = body; + + if (!(await canUpdateWebsite(auth, websiteId))) { + return unauthorized(); + } + + try { + const website = await updateWebsite(websiteId, { name, domain, shareId }); + + return Response.json(website); + } catch (e: any) { + if (e.message.includes('Unique constraint') && e.message.includes('share_id')) { + return serverError(new Error('That share ID is already taken.')); + } + + return serverError(e); + } +} + +export async function DELETE( + request: Request, + { params }: { params: Promise<{ websiteId: string }> }, +) { + const { auth, error } = await parseRequest(request); + + if (error) { + return error(); + } + + const { websiteId } = await params; + + if (!(await canDeleteWebsite(auth, websiteId))) { + return unauthorized(); + } + + await deleteWebsite(websiteId); + + return ok(); +} diff --git a/src/app/api/pixels/route.ts b/src/app/api/pixels/route.ts index e69de29b..f2730196 100644 --- a/src/app/api/pixels/route.ts +++ b/src/app/api/pixels/route.ts @@ -0,0 +1,62 @@ +import { z } from 'zod'; +import { canCreateTeamWebsite, canCreateWebsite } from '@/validations'; +import { json, unauthorized } from '@/lib/response'; +import { uuid } from '@/lib/crypto'; +import { getQueryFilters, parseRequest } from '@/lib/request'; +import { pagingParams, searchParams } from '@/lib/schema'; +import { createPixel, getUserLinks } from '@/queries'; + +export async function GET(request: Request) { + const schema = z.object({ + ...pagingParams, + ...searchParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const filters = await getQueryFilters(query); + + const inks = await getUserLinks(auth.user.id, filters); + + return json(inks); +} + +export async function POST(request: Request) { + const schema = z.object({ + name: z.string().max(100), + slug: z.string().max(100), + teamId: z.string().nullable().optional(), + id: z.string().uuid().nullable().optional(), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { id, name, slug, teamId } = body; + + if ((teamId && !(await canCreateTeamWebsite(auth, teamId))) || !(await canCreateWebsite(auth))) { + return unauthorized(); + } + + const data: any = { + id: id ?? uuid(), + name, + slug, + teamId, + }; + + if (!teamId) { + data.userId = auth.user.id; + } + + const result = await createPixel(data); + + return json(result); +} diff --git a/src/app/api/realtime/[websiteId]/route.ts b/src/app/api/realtime/[websiteId]/route.ts index d06d460a..baca763d 100644 --- a/src/app/api/realtime/[websiteId]/route.ts +++ b/src/app/api/realtime/[websiteId]/route.ts @@ -1,6 +1,6 @@ import { json, unauthorized } from '@/lib/response'; import { getRealtimeData } from '@/queries'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { startOfMinute, subMinutes } from 'date-fns'; import { REALTIME_RANGE } from '@/lib/constants'; import { parseRequest, getQueryFilters } from '@/lib/request'; diff --git a/src/app/api/reports/[reportId]/route.ts b/src/app/api/reports/[reportId]/route.ts index c35c1eb0..5cb7074b 100644 --- a/src/app/api/reports/[reportId]/route.ts +++ b/src/app/api/reports/[reportId]/route.ts @@ -1,6 +1,6 @@ import { parseRequest } from '@/lib/request'; import { deleteReport, getReport, updateReport } from '@/queries'; -import { canDeleteReport, canUpdateReport, canViewReport } from '@/lib/auth'; +import { canDeleteReport, canUpdateReport, canViewReport } from '@/validations'; import { unauthorized, json, notFound, ok } from '@/lib/response'; import { reportSchema } from '@/lib/schema'; diff --git a/src/app/api/reports/attribution/route.ts b/src/app/api/reports/attribution/route.ts index 689c6400..d8032245 100644 --- a/src/app/api/reports/attribution/route.ts +++ b/src/app/api/reports/attribution/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { reportResultSchema } from '@/lib/schema'; diff --git a/src/app/api/reports/breakdown/route.ts b/src/app/api/reports/breakdown/route.ts index f948051d..7db9f923 100644 --- a/src/app/api/reports/breakdown/route.ts +++ b/src/app/api/reports/breakdown/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { BreakdownParameters, getBreakdown } from '@/queries'; diff --git a/src/app/api/reports/funnel/route.ts b/src/app/api/reports/funnel/route.ts index 0acab5c9..d3e386cc 100644 --- a/src/app/api/reports/funnel/route.ts +++ b/src/app/api/reports/funnel/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { FunnelParameters, getFunnel } from '@/queries'; diff --git a/src/app/api/reports/goal/route.ts b/src/app/api/reports/goal/route.ts index 86c80a0e..47e5ba91 100644 --- a/src/app/api/reports/goal/route.ts +++ b/src/app/api/reports/goal/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { getGoal, GoalParameters } from '@/queries/sql/reports/getGoal'; diff --git a/src/app/api/reports/journey/route.ts b/src/app/api/reports/journey/route.ts index 4b31216e..37a470f4 100644 --- a/src/app/api/reports/journey/route.ts +++ b/src/app/api/reports/journey/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { getJourney } from '@/queries'; diff --git a/src/app/api/reports/retention/route.ts b/src/app/api/reports/retention/route.ts index 9367ab70..7297d744 100644 --- a/src/app/api/reports/retention/route.ts +++ b/src/app/api/reports/retention/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { getRetention, RetentionParameters } from '@/queries'; diff --git a/src/app/api/reports/revenue/route.ts b/src/app/api/reports/revenue/route.ts index b8a6ae4c..7e1137bc 100644 --- a/src/app/api/reports/revenue/route.ts +++ b/src/app/api/reports/revenue/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { parseRequest, getQueryFilters, setWebsiteDate } from '@/lib/request'; import { reportResultSchema } from '@/lib/schema'; diff --git a/src/app/api/reports/route.ts b/src/app/api/reports/route.ts index c68c083e..ce7ba50c 100644 --- a/src/app/api/reports/route.ts +++ b/src/app/api/reports/route.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { uuid } from '@/lib/crypto'; import { pagingParams, reportSchema } from '@/lib/schema'; import { parseRequest } from '@/lib/request'; -import { canViewWebsite, canUpdateWebsite } from '@/lib/auth'; +import { canViewWebsite, canUpdateWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { getReports, createReport } from '@/queries'; diff --git a/src/app/api/reports/utm/route.ts b/src/app/api/reports/utm/route.ts index 150773f7..8a4a1747 100644 --- a/src/app/api/reports/utm/route.ts +++ b/src/app/api/reports/utm/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { unauthorized, json } from '@/lib/response'; import { getQueryFilters, parseRequest, setWebsiteDate } from '@/lib/request'; import { getUTM, UTMParameters } from '@/queries'; diff --git a/src/app/api/teams/[teamId]/links/route.ts b/src/app/api/teams/[teamId]/links/route.ts index a0b0dd2c..b3be8a5c 100644 --- a/src/app/api/teams/[teamId]/links/route.ts +++ b/src/app/api/teams/[teamId]/links/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { unauthorized, json } from '@/lib/response'; -import { canViewTeam } from '@/lib/auth'; +import { canViewTeam } from '@/validations'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { pagingParams, searchParams } from '@/lib/schema'; import { getTeamLinks } from '@/queries'; @@ -23,7 +23,7 @@ export async function GET(request: Request, { params }: { params: Promise<{ team const filters = await getQueryFilters(query); - const websites = await getTeamLinks(teamId, filters); + const links = await getTeamLinks(teamId, filters); - return json(websites); + return json(links); } diff --git a/src/app/api/teams/[teamId]/pixels/pixels.ts b/src/app/api/teams/[teamId]/pixels/route.ts similarity index 94% rename from src/app/api/teams/[teamId]/pixels/pixels.ts rename to src/app/api/teams/[teamId]/pixels/route.ts index 872b4a79..41d9ce0f 100644 --- a/src/app/api/teams/[teamId]/pixels/pixels.ts +++ b/src/app/api/teams/[teamId]/pixels/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { unauthorized, json } from '@/lib/response'; -import { canViewTeam } from '@/lib/auth'; +import { canViewTeam } from '@/validations'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { pagingParams, searchParams } from '@/lib/schema'; import { getTeamPixels } from '@/queries'; diff --git a/src/app/api/teams/[teamId]/route.ts b/src/app/api/teams/[teamId]/route.ts index 194e7bbb..caa167e3 100644 --- a/src/app/api/teams/[teamId]/route.ts +++ b/src/app/api/teams/[teamId]/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { unauthorized, json, notFound, ok } from '@/lib/response'; -import { canDeleteTeam, canUpdateTeam, canViewTeam } from '@/lib/auth'; +import { canDeleteTeam, canUpdateTeam, canViewTeam } from '@/validations'; import { parseRequest } from '@/lib/request'; import { deleteTeam, getTeam, updateTeam } from '@/queries'; diff --git a/src/app/api/teams/[teamId]/users/[userId]/route.ts b/src/app/api/teams/[teamId]/users/[userId]/route.ts index bf5f4d36..62e0a32c 100644 --- a/src/app/api/teams/[teamId]/users/[userId]/route.ts +++ b/src/app/api/teams/[teamId]/users/[userId]/route.ts @@ -1,4 +1,4 @@ -import { canDeleteTeamUser, canUpdateTeam } from '@/lib/auth'; +import { canDeleteTeamUser, canUpdateTeam } from '@/validations'; import { parseRequest } from '@/lib/request'; import { badRequest, json, ok, unauthorized } from '@/lib/response'; import { deleteTeamUser, getTeamUser, updateTeamUser } from '@/queries'; diff --git a/src/app/api/teams/[teamId]/users/route.ts b/src/app/api/teams/[teamId]/users/route.ts index 6fde0c66..93e41f60 100644 --- a/src/app/api/teams/[teamId]/users/route.ts +++ b/src/app/api/teams/[teamId]/users/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { unauthorized, json, badRequest } from '@/lib/response'; -import { canAddUserToTeam, canViewTeam } from '@/lib/auth'; +import { canAddUserToTeam, canViewTeam } from '@/validations'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { pagingParams, teamRoleParam, searchParams } from '@/lib/schema'; import { createTeamUser, getTeamUser, getTeamUsers } from '@/queries'; diff --git a/src/app/api/teams/[teamId]/websites/route.ts b/src/app/api/teams/[teamId]/websites/route.ts index f3acde38..594416fa 100644 --- a/src/app/api/teams/[teamId]/websites/route.ts +++ b/src/app/api/teams/[teamId]/websites/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { unauthorized, json } from '@/lib/response'; -import { canViewTeam } from '@/lib/auth'; +import { canViewTeam } from '@/validations'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { pagingParams, searchParams } from '@/lib/schema'; import { getTeamWebsites } from '@/queries'; diff --git a/src/app/api/teams/join/route.ts b/src/app/api/teams/join/route.ts index 3464054c..b17a2c08 100644 --- a/src/app/api/teams/join/route.ts +++ b/src/app/api/teams/join/route.ts @@ -1,6 +1,6 @@ import { z } from 'zod'; import { unauthorized, json, badRequest, notFound } from '@/lib/response'; -import { canCreateTeam } from '@/lib/auth'; +import { canCreateTeam } from '@/validations'; import { parseRequest } from '@/lib/request'; import { ROLES } from '@/lib/constants'; import { createTeamUser, findTeam, getTeamUser } from '@/queries'; diff --git a/src/app/api/teams/route.ts b/src/app/api/teams/route.ts index d319d87b..b6121532 100644 --- a/src/app/api/teams/route.ts +++ b/src/app/api/teams/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getRandomChars } from '@/lib/crypto'; import { unauthorized, json } from '@/lib/response'; -import { canCreateTeam } from '@/lib/auth'; +import { canCreateTeam } from '@/validations'; import { uuid } from '@/lib/crypto'; import { parseRequest } from '@/lib/request'; import { createTeam } from '@/queries'; diff --git a/src/app/api/users/[userId]/route.ts b/src/app/api/users/[userId]/route.ts index 20ce5f23..2a822e1d 100644 --- a/src/app/api/users/[userId]/route.ts +++ b/src/app/api/users/[userId]/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { canUpdateUser, canViewUser, canDeleteUser } from '@/lib/auth'; +import { canUpdateUser, canViewUser, canDeleteUser } from '@/validations'; import { getUser, getUserByUsername, updateUser, deleteUser } from '@/queries'; import { json, unauthorized, badRequest, ok } from '@/lib/response'; import { hashPassword } from '@/lib/auth'; diff --git a/src/app/api/users/route.ts b/src/app/api/users/route.ts index c5896f89..4b7f4d49 100644 --- a/src/app/api/users/route.ts +++ b/src/app/api/users/route.ts @@ -1,5 +1,6 @@ import { z } from 'zod'; -import { hashPassword, canCreateUser } from '@/lib/auth'; +import { hashPassword } from '@/lib/auth'; +import { canCreateUser } from '@/validations'; import { ROLES } from '@/lib/constants'; import { uuid } from '@/lib/crypto'; import { parseRequest } from '@/lib/request'; diff --git a/src/app/api/websites/[websiteId]/active/route.ts b/src/app/api/websites/[websiteId]/active/route.ts index 88c0fd17..c30ce910 100644 --- a/src/app/api/websites/[websiteId]/active/route.ts +++ b/src/app/api/websites/[websiteId]/active/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { json, unauthorized } from '@/lib/response'; import { getActiveVisitors } from '@/queries'; import { parseRequest } from '@/lib/request'; @@ -19,7 +19,7 @@ export async function GET( return unauthorized(); } - const result = await getActiveVisitors(websiteId); + const visitors = await getActiveVisitors(websiteId); - return json(result); + return json(visitors); } diff --git a/src/app/api/websites/[websiteId]/daterange/route.ts b/src/app/api/websites/[websiteId]/daterange/route.ts index ea2d10d2..7c208308 100644 --- a/src/app/api/websites/[websiteId]/daterange/route.ts +++ b/src/app/api/websites/[websiteId]/daterange/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getWebsiteDateRange } from '@/queries'; import { json, unauthorized } from '@/lib/response'; import { parseRequest } from '@/lib/request'; @@ -19,7 +19,7 @@ export async function GET( return unauthorized(); } - const result = await getWebsiteDateRange(websiteId); + const dateRange = await getWebsiteDateRange(websiteId); - return json(result); + return json(dateRange); } diff --git a/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts index 8eeb7171..01258bb4 100644 --- a/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/[eventId]/route.ts @@ -1,6 +1,6 @@ import { parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getEventData } from '@/queries/sql/events/getEventData'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/event-data/events/route.ts b/src/app/api/websites/[websiteId]/event-data/events/route.ts index 2f1de5d3..9511d0d1 100644 --- a/src/app/api/websites/[websiteId]/event-data/events/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/events/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getEventDataEvents } from '@/queries/sql/events/getEventDataEvents'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/event-data/fields/route.ts b/src/app/api/websites/[websiteId]/event-data/fields/route.ts index 1d97a4ba..371d8ec4 100644 --- a/src/app/api/websites/[websiteId]/event-data/fields/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/fields/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getEventDataFields } from '@/queries'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/event-data/properties/route.ts b/src/app/api/websites/[websiteId]/event-data/properties/route.ts index 5043e272..001d5b61 100644 --- a/src/app/api/websites/[websiteId]/event-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/properties/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getEventDataProperties } from '@/queries'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/event-data/stats/route.ts b/src/app/api/websites/[websiteId]/event-data/stats/route.ts index 9b7fe357..5b799153 100644 --- a/src/app/api/websites/[websiteId]/event-data/stats/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/stats/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getEventDataStats } from '@/queries'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/event-data/values/route.ts b/src/app/api/websites/[websiteId]/event-data/values/route.ts index 6210de84..1d8438b7 100644 --- a/src/app/api/websites/[websiteId]/event-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/event-data/values/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getEventDataValues } from '@/queries'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/events/route.ts b/src/app/api/websites/[websiteId]/events/route.ts index 2a57ad07..b863fa9c 100644 --- a/src/app/api/websites/[websiteId]/events/route.ts +++ b/src/app/api/websites/[websiteId]/events/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { dateRangeParams, pagingParams, filterParams, searchParams } from '@/lib/schema'; import { getWebsiteEvents } from '@/queries'; diff --git a/src/app/api/websites/[websiteId]/events/series/route.ts b/src/app/api/websites/[websiteId]/events/series/route.ts index f85f24a5..542fe07b 100644 --- a/src/app/api/websites/[websiteId]/events/series/route.ts +++ b/src/app/api/websites/[websiteId]/events/series/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest, getQueryFilters } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { filterParams, timezoneParam, unitParam } from '@/lib/schema'; import { getEventStats } from '@/queries'; diff --git a/src/app/api/websites/[websiteId]/export/route.ts b/src/app/api/websites/[websiteId]/export/route.ts index ab9fdd12..4247da7d 100644 --- a/src/app/api/websites/[websiteId]/export/route.ts +++ b/src/app/api/websites/[websiteId]/export/route.ts @@ -3,7 +3,7 @@ import JSZip from 'jszip'; import Papa from 'papaparse'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { pagingParams, dateRangeParams } from '@/lib/schema'; import { getEventMetrics, getPageviewMetrics, getSessionMetrics } from '@/queries'; diff --git a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts index a327bd0b..245d144a 100644 --- a/src/app/api/websites/[websiteId]/metrics/expanded/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/expanded/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { EVENT_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { badRequest, json, unauthorized } from '@/lib/response'; diff --git a/src/app/api/websites/[websiteId]/metrics/route.ts b/src/app/api/websites/[websiteId]/metrics/route.ts index bc295d79..ea76b4e8 100644 --- a/src/app/api/websites/[websiteId]/metrics/route.ts +++ b/src/app/api/websites/[websiteId]/metrics/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { EVENT_COLUMNS, SESSION_COLUMNS } from '@/lib/constants'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { badRequest, json, unauthorized } from '@/lib/response'; diff --git a/src/app/api/websites/[websiteId]/pageviews/route.ts b/src/app/api/websites/[websiteId]/pageviews/route.ts index 83175d0e..bfcc8f07 100644 --- a/src/app/api/websites/[websiteId]/pageviews/route.ts +++ b/src/app/api/websites/[websiteId]/pageviews/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { dateRangeParams, filterParams } from '@/lib/schema'; import { getCompareDate } from '@/lib/date'; diff --git a/src/app/api/websites/[websiteId]/reports/route.ts b/src/app/api/websites/[websiteId]/reports/route.ts index 88455685..60e9b2d9 100644 --- a/src/app/api/websites/[websiteId]/reports/route.ts +++ b/src/app/api/websites/[websiteId]/reports/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getReports } from '@/queries'; import { filterParams, pagingParams } from '@/lib/schema'; import { parseRequest } from '@/lib/request'; diff --git a/src/app/api/websites/[websiteId]/reset/route.ts b/src/app/api/websites/[websiteId]/reset/route.ts index 62edceea..36732205 100644 --- a/src/app/api/websites/[websiteId]/reset/route.ts +++ b/src/app/api/websites/[websiteId]/reset/route.ts @@ -1,4 +1,4 @@ -import { canUpdateWebsite } from '@/lib/auth'; +import { canUpdateWebsite } from '@/validations'; import { resetWebsite } from '@/queries'; import { unauthorized, ok } from '@/lib/response'; import { parseRequest } from '@/lib/request'; diff --git a/src/app/api/websites/[websiteId]/route.ts b/src/app/api/websites/[websiteId]/route.ts index 4f8763b5..e57a90b0 100644 --- a/src/app/api/websites/[websiteId]/route.ts +++ b/src/app/api/websites/[websiteId]/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/lib/auth'; +import { canUpdateWebsite, canDeleteWebsite, canViewWebsite } from '@/validations'; import { SHARE_ID_REGEX } from '@/lib/constants'; import { parseRequest } from '@/lib/request'; import { ok, json, unauthorized, serverError } from '@/lib/response'; diff --git a/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts index fd2442cb..588a4cac 100644 --- a/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts +++ b/src/app/api/websites/[websiteId]/segments/[segmentId]/route.ts @@ -1,4 +1,4 @@ -import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/lib/auth'; +import { canDeleteWebsite, canUpdateWebsite, canViewWebsite } from '@/validations'; import { parseRequest } from '@/lib/request'; import { json, notFound, ok, unauthorized } from '@/lib/response'; import { segmentTypeParam } from '@/lib/schema'; diff --git a/src/app/api/websites/[websiteId]/segments/route.ts b/src/app/api/websites/[websiteId]/segments/route.ts index 5bbcceed..ed89e55f 100644 --- a/src/app/api/websites/[websiteId]/segments/route.ts +++ b/src/app/api/websites/[websiteId]/segments/route.ts @@ -1,4 +1,4 @@ -import { canUpdateWebsite, canViewWebsite } from '@/lib/auth'; +import { canUpdateWebsite, canViewWebsite } from '@/validations'; import { uuid } from '@/lib/crypto'; import { parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; diff --git a/src/app/api/websites/[websiteId]/session-data/properties/route.ts b/src/app/api/websites/[websiteId]/session-data/properties/route.ts index b51f1c2f..5c7e2c47 100644 --- a/src/app/api/websites/[websiteId]/session-data/properties/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/properties/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getSessionDataProperties } from '@/queries'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/session-data/values/route.ts b/src/app/api/websites/[websiteId]/session-data/values/route.ts index 858aa5e3..27e95251 100644 --- a/src/app/api/websites/[websiteId]/session-data/values/route.ts +++ b/src/app/api/websites/[websiteId]/session-data/values/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { json, unauthorized } from '@/lib/response'; import { getSessionDataValues } from '@/queries'; diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts index 268e7bf8..ca226120 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/activity/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest, getQueryFilters } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getSessionActivity } from '@/queries'; export async function GET( diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts index 9c389c82..21663462 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/properties/route.ts @@ -1,5 +1,5 @@ import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getSessionData } from '@/queries'; import { parseRequest } from '@/lib/request'; diff --git a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts index c4621ef4..d110ebeb 100644 --- a/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/[sessionId]/route.ts @@ -1,5 +1,5 @@ import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { getWebsiteSession } from '@/queries'; import { parseRequest } from '@/lib/request'; diff --git a/src/app/api/websites/[websiteId]/sessions/route.ts b/src/app/api/websites/[websiteId]/sessions/route.ts index 5f81e14c..6a8c071b 100644 --- a/src/app/api/websites/[websiteId]/sessions/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { dateRangeParams, filterParams, pagingParams, searchParams } from '@/lib/schema'; import { getWebsiteSessions } from '@/queries'; diff --git a/src/app/api/websites/[websiteId]/sessions/stats/route.ts b/src/app/api/websites/[websiteId]/sessions/stats/route.ts index d78be852..619a05ad 100644 --- a/src/app/api/websites/[websiteId]/sessions/stats/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/stats/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest, getQueryFilters } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { filterParams } from '@/lib/schema'; import { getWebsiteSessionStats } from '@/queries'; diff --git a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts index 2c7b227f..5494b5a9 100644 --- a/src/app/api/websites/[websiteId]/sessions/weekly/route.ts +++ b/src/app/api/websites/[websiteId]/sessions/weekly/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { pagingParams, timezoneParam } from '@/lib/schema'; import { getWebsiteSessionsWeekly } from '@/queries'; diff --git a/src/app/api/websites/[websiteId]/stats/route.ts b/src/app/api/websites/[websiteId]/stats/route.ts index 924d189e..302346c2 100644 --- a/src/app/api/websites/[websiteId]/stats/route.ts +++ b/src/app/api/websites/[websiteId]/stats/route.ts @@ -1,7 +1,7 @@ import { z } from 'zod'; import { parseRequest, getQueryFilters } from '@/lib/request'; import { unauthorized, json } from '@/lib/response'; -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { dateRangeParams, filterParams } from '@/lib/schema'; import { getWebsiteStats } from '@/queries'; import { getCompareDate } from '@/lib/date'; diff --git a/src/app/api/websites/[websiteId]/transfer/route.ts b/src/app/api/websites/[websiteId]/transfer/route.ts index 03c0ae7f..2a2826ae 100644 --- a/src/app/api/websites/[websiteId]/transfer/route.ts +++ b/src/app/api/websites/[websiteId]/transfer/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/lib/auth'; +import { canTransferWebsiteToTeam, canTransferWebsiteToUser } from '@/validations'; import { updateWebsite } from '@/queries'; import { parseRequest } from '@/lib/request'; import { badRequest, unauthorized, json } from '@/lib/response'; diff --git a/src/app/api/websites/[websiteId]/values/route.ts b/src/app/api/websites/[websiteId]/values/route.ts index e4140e14..91870bd5 100644 --- a/src/app/api/websites/[websiteId]/values/route.ts +++ b/src/app/api/websites/[websiteId]/values/route.ts @@ -1,4 +1,4 @@ -import { canViewWebsite } from '@/lib/auth'; +import { canViewWebsite } from '@/validations'; import { EVENT_COLUMNS, FILTER_COLUMNS, FILTER_GROUPS, SESSION_COLUMNS } from '@/lib/constants'; import { getQueryFilters, parseRequest } from '@/lib/request'; import { badRequest, json, unauthorized } from '@/lib/response'; diff --git a/src/app/api/websites/route.ts b/src/app/api/websites/route.ts index 96c45333..963f8531 100644 --- a/src/app/api/websites/route.ts +++ b/src/app/api/websites/route.ts @@ -1,5 +1,5 @@ import { z } from 'zod'; -import { canCreateTeamWebsite, canCreateWebsite } from '@/lib/auth'; +import { canCreateTeamWebsite, canCreateWebsite } from '@/validations'; import { json, unauthorized } from '@/lib/response'; import { uuid } from '@/lib/crypto'; import { parseRequest } from '@/lib/request'; diff --git a/src/components/common/DateDistance.tsx b/src/components/common/DateDistance.tsx index 43056b0c..b305f857 100644 --- a/src/components/common/DateDistance.tsx +++ b/src/components/common/DateDistance.tsx @@ -1,4 +1,4 @@ -import { Tooltip, TooltipTrigger, Text, Focusable } from '@umami/react-zen'; +import { Text } from '@umami/react-zen'; import { formatDistanceToNow } from 'date-fns'; import { useLocale, useTimezone } from '@/components/hooks'; @@ -7,11 +7,8 @@ export function DateDistance({ date }: { date: Date }) { const { dateLocale } = useLocale(); return ( - - - {formatDistanceToNow(date, { addSuffix: true, locale: dateLocale })} - - {formatTimezoneDate(date.toISOString(), 'PPPpp')} - + + {formatDistanceToNow(date, { addSuffix: true, locale: dateLocale })} + ); } diff --git a/src/components/common/ExternalLink.tsx b/src/components/common/ExternalLink.tsx new file mode 100644 index 00000000..b3cea360 --- /dev/null +++ b/src/components/common/ExternalLink.tsx @@ -0,0 +1,18 @@ +import Link from 'next/link'; +import { Icon, Row, Text } from '@umami/react-zen'; +import { ExternalLink as LinkIcon } from '@/components/icons'; + +export function ExternalLink({ href, children, ...props }: Icon) { + return ( + + + + {children} + + + + + + + ); +} diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index bead3b42..8505f29d 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -1,8 +1,11 @@ 'use client'; + +// Query hooks export * from './queries/useActiveUsersQuery'; -export * from './queries/useEventDataQuery'; +export * from './queries/useDeleteQuery'; export * from './queries/useEventDataEventsQuery'; export * from './queries/useEventDataPropertiesQuery'; +export * from './queries/useEventDataQuery'; export * from './queries/useEventDataValuesQuery'; export * from './queries/useLinkQuery'; export * from './queries/useLinksQuery'; @@ -10,41 +13,51 @@ export * from './queries/useLoginQuery'; export * from './queries/usePixelQuery'; export * from './queries/usePixelsQuery'; export * from './queries/useRealtimeQuery'; -export * from './queries/useResultQuery'; export * from './queries/useReportQuery'; export * from './queries/useReportsQuery'; +export * from './queries/useResultQuery'; export * from './queries/useSessionActivityQuery'; -export * from './queries/useSessionDataQuery'; export * from './queries/useSessionDataPropertiesQuery'; +export * from './queries/useSessionDataQuery'; export * from './queries/useSessionDataValuesQuery'; -export * from './queries/useWebsiteSessionQuery'; -export * from './queries/useWebsiteSessionsQuery'; -export * from './queries/useWebsiteSessionsWeeklyQuery'; export * from './queries/useShareTokenQuery'; +export * from './queries/useTeamMembersQuery'; export * from './queries/useTeamQuery'; +export * from './queries/useTeamWebsitesQuery'; export * from './queries/useTeamsQuery'; +export * from './queries/useUpdateQuery'; +export * from './queries/useUserQuery'; export * from './queries/useUserTeamsQuery'; export * from './queries/useUserWebsitesQuery'; -export * from './queries/useTeamWebsitesQuery'; -export * from './queries/useTeamMembersQuery'; -export * from './queries/useUserQuery'; export * from './queries/useUsersQuery'; +export * from './queries/useWebsiteCohortQuery'; +export * from './queries/useWebsiteCohortsQuery'; +export * from './queries/useWebsiteEventsQuery'; +export * from './queries/useWebsiteEventsSeriesQuery'; +export * from './queries/useWebsiteExpandedMetricsQuery'; +export * from './queries/useWebsiteMetricsQuery'; +export * from './queries/useWebsitePageviewsQuery'; export * from './queries/useWebsiteQuery'; export * from './queries/useWebsiteSegmentQuery'; export * from './queries/useWebsiteSegmentsQuery'; -export * from './queries/useWebsitesQuery'; -export * from './queries/useWebsiteEventsQuery'; -export * from './queries/useWebsiteEventsSeriesQuery'; -export * from './queries/useWebsiteMetricsQuery'; -export * from './queries/useWebsiteExpandedMetricsQuery'; +export * from './queries/useWebsiteSessionQuery'; +export * from './queries/useWebsiteSessionStatsQuery'; +export * from './queries/useWebsiteSessionsQuery'; +export * from './queries/useWebsiteSessionsWeeklyQuery'; +export * from './queries/useWebsiteStatsQuery'; export * from './queries/useWebsiteValuesQuery'; +export * from './queries/useWebsitesQuery'; + +// Regular hooks export * from './useApi'; export * from './useConfig'; export * from './useCountryNames'; +export * from './useDateParameters'; export * from './useDateRange'; export * from './useDocumentClick'; export * from './useEscapeKey'; export * from './useFields'; +export * from './useFilterParameters'; export * from './useFilters'; export * from './useForceUpdate'; export * from './useFormat'; @@ -55,6 +68,7 @@ export * from './useMessages'; export * from './useModified'; export * from './useNavigation'; export * from './usePagedQuery'; +export * from './usePageParameters'; export * from './useRegionNames'; export * from './useSticky'; export * from './useTeam'; diff --git a/src/components/hooks/queries/useActiveUsersQuery.ts b/src/components/hooks/queries/useActiveUsersQuery.ts index 9335b75a..d0538dc7 100644 --- a/src/components/hooks/queries/useActiveUsersQuery.ts +++ b/src/components/hooks/queries/useActiveUsersQuery.ts @@ -1,7 +1,7 @@ import { useApi } from '../useApi'; import { ReactQueryOptions } from '@/lib/types'; -export function useActyiveUsersQuery(websiteId: string, options?: ReactQueryOptions) { +export function useActyiveUsersQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); return useQuery({ queryKey: ['websites:active', websiteId], diff --git a/src/components/hooks/queries/useEventDataEventsQuery.ts b/src/components/hooks/queries/useEventDataEventsQuery.ts index 41d34f74..5273181e 100644 --- a/src/components/hooks/queries/useEventDataEventsQuery.ts +++ b/src/components/hooks/queries/useEventDataEventsQuery.ts @@ -3,7 +3,7 @@ import { useFilterParameters } from '../useFilterParameters'; import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; -export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) { +export function useEventDataEventsQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); const filters = useFilterParameters(); diff --git a/src/components/hooks/queries/useEventDataPropertiesQuery.ts b/src/components/hooks/queries/useEventDataPropertiesQuery.ts index 31618940..26929700 100644 --- a/src/components/hooks/queries/useEventDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useEventDataPropertiesQuery.ts @@ -3,7 +3,7 @@ import { useFilterParameters } from '../useFilterParameters'; import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; -export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { +export function useEventDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); const filters = useFilterParameters(); diff --git a/src/components/hooks/queries/useEventDataQuery.ts b/src/components/hooks/queries/useEventDataQuery.ts index 05b095f3..00f5d14d 100644 --- a/src/components/hooks/queries/useEventDataQuery.ts +++ b/src/components/hooks/queries/useEventDataQuery.ts @@ -3,11 +3,7 @@ import { useFilterParameters } from '../useFilterParameters'; import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; -export function useEventDataQuery( - websiteId: string, - eventId: string, - options?: ReactQueryOptions, -) { +export function useEventDataQuery(websiteId: string, eventId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); const params = useFilterParameters(); diff --git a/src/components/hooks/queries/useEventDataValuesQuery.ts b/src/components/hooks/queries/useEventDataValuesQuery.ts index 0bf87f86..4b56e553 100644 --- a/src/components/hooks/queries/useEventDataValuesQuery.ts +++ b/src/components/hooks/queries/useEventDataValuesQuery.ts @@ -7,7 +7,7 @@ export function useEventDataValuesQuery( websiteId: string, eventName: string, propertyName: string, - options?: ReactQueryOptions, + options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); diff --git a/src/components/hooks/queries/useLinksQuery.ts b/src/components/hooks/queries/useLinksQuery.ts index 447afb7f..93de47dc 100644 --- a/src/components/hooks/queries/useLinksQuery.ts +++ b/src/components/hooks/queries/useLinksQuery.ts @@ -3,13 +3,15 @@ import { usePagedQuery } from '../usePagedQuery'; import { useModified } from '../useModified'; import { ReactQueryOptions } from '@/lib/types'; -export function useLinksQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) { +export function useLinksQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) { const { modified } = useModified('links'); const { get } = useApi(); return usePagedQuery({ queryKey: ['links', { teamId, modified }], - queryFn: async () => get(teamId ? `/teams/${teamId}/links` : '/links'), + queryFn: pageParams => { + return get(teamId ? `/teams/${teamId}/links` : '/links', pageParams); + }, ...options, }); } diff --git a/src/components/hooks/queries/usePixelsQuery.ts b/src/components/hooks/queries/usePixelsQuery.ts index f1451873..466b13d0 100644 --- a/src/components/hooks/queries/usePixelsQuery.ts +++ b/src/components/hooks/queries/usePixelsQuery.ts @@ -3,17 +3,15 @@ import { usePagedQuery } from '../usePagedQuery'; import { useModified } from '../useModified'; import { ReactQueryOptions } from '@/lib/types'; -export function usePixelsQuery( - { websiteId, type }: { websiteId: string; type?: string }, - options?: ReactQueryOptions, -) { - const { modified } = useModified(`pixels:${type}`); +export function usePixelsQuery({ teamId }: { teamId?: string }, options?: ReactQueryOptions) { + const { modified } = useModified('pixels'); const { get } = useApi(); return usePagedQuery({ - queryKey: ['pixels', { websiteId, type, modified }], - queryFn: async () => get('/pixels', { websiteId, type }), - enabled: !!websiteId && !!type, + queryKey: ['pixels', { teamId, modified }], + queryFn: pageParams => { + return get(teamId ? `/teams/${teamId}/pixels` : '/pixels', pageParams); + }, ...options, }); } diff --git a/src/components/hooks/queries/useReportsQuery.ts b/src/components/hooks/queries/useReportsQuery.ts index 8c05794f..fed15014 100644 --- a/src/components/hooks/queries/useReportsQuery.ts +++ b/src/components/hooks/queries/useReportsQuery.ts @@ -5,7 +5,7 @@ import { ReactQueryOptions } from '@/lib/types'; export function useReportsQuery( { websiteId, type }: { websiteId: string; type?: string }, - options?: ReactQueryOptions, + options?: ReactQueryOptions, ) { const { modified } = useModified(`reports:${type}`); const { get } = useApi(); diff --git a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts index 6c54638c..633a6ff8 100644 --- a/src/components/hooks/queries/useSessionDataPropertiesQuery.ts +++ b/src/components/hooks/queries/useSessionDataPropertiesQuery.ts @@ -3,7 +3,7 @@ import { useFilterParameters } from '../useFilterParameters'; import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; -export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { +export function useSessionDataPropertiesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); const filters = useFilterParameters(); diff --git a/src/components/hooks/queries/useSessionDataValuesQuery.ts b/src/components/hooks/queries/useSessionDataValuesQuery.ts index 212d7628..bf7a8cef 100644 --- a/src/components/hooks/queries/useSessionDataValuesQuery.ts +++ b/src/components/hooks/queries/useSessionDataValuesQuery.ts @@ -6,7 +6,7 @@ import { ReactQueryOptions } from '@/lib/types'; export function useSessionDataValuesQuery( websiteId: string, propertyName: string, - options?: ReactQueryOptions, + options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); diff --git a/src/components/hooks/queries/useTeamQuery.ts b/src/components/hooks/queries/useTeamQuery.ts index f471172b..497e3b36 100644 --- a/src/components/hooks/queries/useTeamQuery.ts +++ b/src/components/hooks/queries/useTeamQuery.ts @@ -3,7 +3,7 @@ import { useModified } from '@/components/hooks'; import { keepPreviousData } from '@tanstack/react-query'; import { ReactQueryOptions } from '@/lib/types'; -export function useTeamQuery(teamId: string, options?: ReactQueryOptions) { +export function useTeamQuery(teamId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const { modified } = useModified(`teams:${teamId}`); diff --git a/src/components/hooks/queries/useUserQuery.ts b/src/components/hooks/queries/useUserQuery.ts index 6fb476d5..3b687f39 100644 --- a/src/components/hooks/queries/useUserQuery.ts +++ b/src/components/hooks/queries/useUserQuery.ts @@ -3,7 +3,7 @@ import { useModified } from '@/components/hooks'; import { keepPreviousData } from '@tanstack/react-query'; import { ReactQueryOptions } from '@/lib/types'; -export function useUserQuery(userId: string, options?: ReactQueryOptions) { +export function useUserQuery(userId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const { modified } = useModified(`user:${userId}`); diff --git a/src/components/hooks/queries/useUserWebsitesQuery.ts b/src/components/hooks/queries/useUserWebsitesQuery.ts index 16c28518..c6312700 100644 --- a/src/components/hooks/queries/useUserWebsitesQuery.ts +++ b/src/components/hooks/queries/useUserWebsitesQuery.ts @@ -1,6 +1,5 @@ import { useApi } from '../useApi'; import { usePagedQuery } from '../usePagedQuery'; -import { useLoginQuery } from './useLoginQuery'; import { useModified } from '../useModified'; import { ReactQueryOptions } from '@/lib/types'; @@ -10,16 +9,22 @@ export function useUserWebsitesQuery( options?: ReactQueryOptions, ) { const { get } = useApi(); - const { user } = useLoginQuery(); const { modified } = useModified(`websites`); return usePagedQuery({ queryKey: ['websites', { userId, teamId, modified, ...params }], queryFn: pageParams => { - return get(teamId ? `/teams/${teamId}/websites` : `/users/${userId || user.id}/websites`, { - ...pageParams, - ...params, - }); + return get( + teamId + ? `/teams/${teamId}/websites` + : userId + ? `/users/${userId}/websites` + : '/me/websites', + { + ...pageParams, + ...params, + }, + ); }, ...options, }); diff --git a/src/components/hooks/queries/useWebsiteCohortQuery.ts b/src/components/hooks/queries/useWebsiteCohortQuery.ts new file mode 100644 index 00000000..489f3c2b --- /dev/null +++ b/src/components/hooks/queries/useWebsiteCohortQuery.ts @@ -0,0 +1,21 @@ +import { useApi } from '../useApi'; +import { useModified } from '@/components/hooks'; +import { keepPreviousData } from '@tanstack/react-query'; +import { ReactQueryOptions } from '@/lib/types'; + +export function useWebsiteCohortQuery( + websiteId: string, + cohortId: string, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { modified } = useModified(`cohorts`); + + return useQuery({ + queryKey: ['website:cohorts', { websiteId, cohortId, modified }], + queryFn: () => get(`/websites/${websiteId}/cohorts/${cohortId}`), + enabled: !!(websiteId && cohortId), + placeholderData: keepPreviousData, + ...options, + }); +} diff --git a/src/components/hooks/queries/useWebsiteCohortsQuery.ts b/src/components/hooks/queries/useWebsiteCohortsQuery.ts new file mode 100644 index 00000000..d20d4465 --- /dev/null +++ b/src/components/hooks/queries/useWebsiteCohortsQuery.ts @@ -0,0 +1,25 @@ +import { useApi } from '../useApi'; +import { useModified } from '@/components/hooks'; +import { keepPreviousData } from '@tanstack/react-query'; +import { ReactQueryOptions } from '@/lib/types'; +import { useFilterParameters } from '@/components/hooks/useFilterParameters'; + +export function useWebsiteCohortsQuery( + websiteId: string, + params?: Record, + options?: ReactQueryOptions, +) { + const { get, useQuery } = useApi(); + const { modified } = useModified(`cohorts`); + const filters = useFilterParameters(); + + return useQuery({ + queryKey: ['website:cohorts', { websiteId, modified, ...filters, ...params }], + queryFn: pageParams => { + return get(`/websites/${websiteId}/cohorts`, { ...pageParams, ...filters, ...params }); + }, + enabled: !!websiteId, + placeholderData: keepPreviousData, + ...options, + }); +} diff --git a/src/components/hooks/queries/useWebsiteEventsQuery.ts b/src/components/hooks/queries/useWebsiteEventsQuery.ts index 6a21f6df..0798524c 100644 --- a/src/components/hooks/queries/useWebsiteEventsQuery.ts +++ b/src/components/hooks/queries/useWebsiteEventsQuery.ts @@ -12,7 +12,7 @@ const EVENT_TYPES = { export function useWebsiteEventsQuery( websiteId: string, params?: Record, - options?: ReactQueryOptions, + options?: ReactQueryOptions, ) { const { get } = useApi(); const date = useDateParameters(websiteId); diff --git a/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts index 8d03b7ac..86fbe428 100644 --- a/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts +++ b/src/components/hooks/queries/useWebsiteEventsSeriesQuery.ts @@ -3,7 +3,7 @@ import { useFilterParameters } from '../useFilterParameters'; import { useDateParameters } from '../useDateParameters'; import { ReactQueryOptions } from '@/lib/types'; -export function useWebsiteEventsSeriesQuery(websiteId: string, options?: ReactQueryOptions) { +export function useWebsiteEventsSeriesQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const date = useDateParameters(websiteId); const filters = useFilterParameters(); diff --git a/src/components/hooks/queries/useWebsiteQuery.ts b/src/components/hooks/queries/useWebsiteQuery.ts index 8faaaa26..a1183593 100644 --- a/src/components/hooks/queries/useWebsiteQuery.ts +++ b/src/components/hooks/queries/useWebsiteQuery.ts @@ -3,7 +3,7 @@ import { useModified } from '@/components/hooks'; import { keepPreviousData } from '@tanstack/react-query'; import { ReactQueryOptions } from '@/lib/types'; -export function useWebsiteQuery(websiteId: string, options?: ReactQueryOptions) { +export function useWebsiteQuery(websiteId: string, options?: ReactQueryOptions) { const { get, useQuery } = useApi(); const { modified } = useModified(`website:${websiteId}`); diff --git a/src/components/hooks/queries/useWebsiteSegmentQuery.ts b/src/components/hooks/queries/useWebsiteSegmentQuery.ts index 5ac6fae9..95a4eb2d 100644 --- a/src/components/hooks/queries/useWebsiteSegmentQuery.ts +++ b/src/components/hooks/queries/useWebsiteSegmentQuery.ts @@ -6,7 +6,7 @@ import { ReactQueryOptions } from '@/lib/types'; export function useWebsiteSegmentQuery( websiteId: string, segmentId: string, - options?: ReactQueryOptions, + options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); const { modified } = useModified(`segments`); diff --git a/src/components/hooks/queries/useWebsiteSegmentsQuery.ts b/src/components/hooks/queries/useWebsiteSegmentsQuery.ts index f64b6c2f..6b4f2bc4 100644 --- a/src/components/hooks/queries/useWebsiteSegmentsQuery.ts +++ b/src/components/hooks/queries/useWebsiteSegmentsQuery.ts @@ -7,7 +7,7 @@ import { useFilterParameters } from '@/components/hooks/useFilterParameters'; export function useWebsiteSegmentsQuery( websiteId: string, params?: Record, - options?: ReactQueryOptions, + options?: ReactQueryOptions, ) { const { get, useQuery } = useApi(); const { modified } = useModified(`segments`); diff --git a/src/components/hooks/uesPageParameters.ts b/src/components/hooks/usePageParameters.ts similarity index 100% rename from src/components/hooks/uesPageParameters.ts rename to src/components/hooks/usePageParameters.ts diff --git a/src/components/input/ActionButton.tsx b/src/components/input/ActionButton.tsx index 7798aa48..ed20fc08 100644 --- a/src/components/input/ActionButton.tsx +++ b/src/components/input/ActionButton.tsx @@ -1,25 +1,24 @@ import { ReactNode } from 'react'; -import { Button, Icon, Modal, DialogTrigger, TooltipTrigger, Tooltip } from '@umami/react-zen'; +import { Button, Icon, Modal, Text, DialogTrigger } from '@umami/react-zen'; export function ActionButton({ onClick, icon, - tooltip, + title, children, }: { onSave?: () => void; icon?: ReactNode; - tooltip?: string; - children?: React.ReactNode; + title?: string; + children?: ReactNode; }) { return ( - + - {tooltip} - + {children} ); diff --git a/src/components/messages.ts b/src/components/messages.ts index d034a3d5..30055f61 100644 --- a/src/components/messages.ts +++ b/src/components/messages.ts @@ -352,6 +352,7 @@ export const labels = defineMessages({ saveCohort: { id: 'label.save-cohort', defaultMessage: 'Save as cohort' }, analysis: { id: 'label.analysis', defaultMessage: 'Analysis' }, destinationUrl: { id: 'label.destination-url', defaultMessage: 'Destination URL' }, + audience: { id: 'label.audience', defaultMessage: 'Audience' }, }); export const messages = defineMessages({ diff --git a/src/lib/auth.ts b/src/lib/auth.ts index b30c254e..947db098 100644 --- a/src/lib/auth.ts +++ b/src/lib/auth.ts @@ -1,16 +1,13 @@ import bcrypt from 'bcryptjs'; -import { Report } from '@prisma/client'; import redis from '@/lib/redis'; import debug from 'debug'; -import { PERMISSIONS, ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants'; +import { ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants'; import { secret, getRandomChars } from '@/lib/crypto'; import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt'; import { ensureArray } from '@/lib/utils'; -import { getTeamUser, getUser, getWebsite } from '@/queries'; -import { Auth } from './types'; +import { getUser } from '@/queries'; const log = debug('umami:auth'); -const cloudMode = process.env.CLOUD_MODE; const SALT_ROUNDS = 10; export function hashPassword(password: string, rounds = SALT_ROUNDS) { @@ -75,6 +72,10 @@ export async function saveAuth(data: any, expire = 0) { return createSecureToken({ authKey }, secret()); } +export async function hasPermission(role: string, permission: string | string[]) { + return ensureArray(permission).some(e => ROLE_PERMISSIONS[role]?.includes(e)); +} + export function parseShareToken(headers: Headers) { try { return parseToken(headers.get(SHARE_TOKEN_HEADER), secret()); @@ -83,243 +84,3 @@ export function parseShareToken(headers: Headers) { return null; } } - -export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) { - if (user?.isAdmin) { - return true; - } - - if (shareToken?.websiteId === websiteId) { - return true; - } - - const website = await getWebsite(websiteId); - - if (website.userId) { - return user.id === website.userId; - } - - if (website.teamId) { - const teamUser = await getTeamUser(website.teamId, user.id); - - return !!teamUser; - } - - return false; -} - -export async function canViewAllWebsites({ user }: Auth) { - return user.isAdmin; -} - -export async function canCreateWebsite({ user, grant }: Auth) { - if (cloudMode) { - return !!grant?.find(a => a === PERMISSIONS.websiteCreate); - } - - if (user.isAdmin) { - return true; - } - - return hasPermission(user.role, PERMISSIONS.websiteCreate); -} - -export async function canUpdateWebsite({ user }: Auth, websiteId: string) { - if (user.isAdmin) { - return true; - } - - const website = await getWebsite(websiteId); - - if (website.userId) { - return user.id === website.userId; - } - - if (website.teamId) { - const teamUser = await getTeamUser(website.teamId, user.id); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteUpdate); - } - - return false; -} - -export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) { - const website = await getWebsite(websiteId); - - if (website.teamId && user.id === userId) { - const teamUser = await getTeamUser(website.teamId, userId); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteTransferToUser); - } - - return false; -} - -export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) { - const website = await getWebsite(websiteId); - - if (website.userId && website.userId === user.id) { - const teamUser = await getTeamUser(teamId, user.id); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteTransferToTeam); - } - - return false; -} - -export async function canDeleteWebsite({ user }: Auth, websiteId: string) { - if (user.isAdmin) { - return true; - } - - const website = await getWebsite(websiteId); - - if (website.userId) { - return user.id === website.userId; - } - - if (website.teamId) { - const teamUser = await getTeamUser(website.teamId, user.id); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteDelete); - } - - return false; -} - -export async function canViewReport(auth: Auth, report: Report) { - if (auth.user.isAdmin) { - return true; - } - - if (auth.user.id == report.userId) { - return true; - } - - return !!(await canViewWebsite(auth, report.websiteId)); -} - -export async function canUpdateReport({ user }: Auth, report: Report) { - if (user.isAdmin) { - return true; - } - - return user.id == report.userId; -} - -export async function canDeleteReport(auth: Auth, report: Report) { - return canUpdateReport(auth, report); -} - -export async function canCreateTeam({ user, grant }: Auth) { - if (cloudMode) { - return !!grant?.find(a => a === PERMISSIONS.teamCreate); - } - - if (user.isAdmin) { - return true; - } - - return !!user; -} - -export async function canViewTeam({ user }: Auth, teamId: string) { - if (user.isAdmin) { - return true; - } - - return getTeamUser(teamId, user.id); -} - -export async function canUpdateTeam({ user, grant }: Auth, teamId: string) { - if (user.isAdmin) { - return true; - } - - if (cloudMode) { - return !!grant?.find(a => a === PERMISSIONS.teamUpdate); - } - - const teamUser = await getTeamUser(teamId, user.id); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamUpdate); -} - -export async function canAddUserToTeam({ user, grant }: Auth) { - if (cloudMode) { - return !!grant?.find(a => a === PERMISSIONS.teamUpdate); - } - - return user.isAdmin; -} - -export async function canDeleteTeam({ user }: Auth, teamId: string) { - if (user.isAdmin) { - return true; - } - - const teamUser = await getTeamUser(teamId, user.id); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamDelete); -} - -export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) { - if (user.isAdmin) { - return true; - } - - if (removeUserId === user.id) { - return true; - } - - const teamUser = await getTeamUser(teamId, user.id); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamUpdate); -} - -export async function canCreateTeamWebsite({ user }: Auth, teamId: string) { - if (user.isAdmin) { - return true; - } - - const teamUser = await getTeamUser(teamId, user.id); - - return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteCreate); -} - -export async function canViewAllTeams({ user }: Auth) { - return user.isAdmin; -} - -export async function canCreateUser({ user }: Auth) { - return user.isAdmin; -} - -export async function canViewUser({ user }: Auth, viewedUserId: string) { - if (user.isAdmin) { - return true; - } - - return user.id === viewedUserId; -} - -export async function canViewUsers({ user }: Auth) { - return user.isAdmin; -} - -export async function canUpdateUser({ user }: Auth, viewedUserId: string) { - if (user.isAdmin) { - return true; - } - - return user.id === viewedUserId; -} - -export async function canDeleteUser({ user }: Auth) { - return user.isAdmin; -} - -export async function hasPermission(role: string, permission: string | string[]) { - return ensureArray(permission).some(e => ROLE_PERMISSIONS[role]?.includes(e)); -} diff --git a/src/queries/prisma/link.ts b/src/queries/prisma/link.ts index 0d887900..5f9ffcfa 100644 --- a/src/queries/prisma/link.ts +++ b/src/queries/prisma/link.ts @@ -19,13 +19,18 @@ export async function getLinks( filters: QueryFilters = {}, ): Promise> { const { search } = filters; + const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.LinkWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(search, [{ name: 'contains' }]), + ...getSearchParameters(search, [ + { name: 'contains' }, + { url: 'contains' }, + { slug: 'contains' }, + ]), }; - return prisma.pagedQuery('link', { ...criteria, where }, filters); + return pagedQuery('link', { ...criteria, where }, filters); } export async function getUserLinks( @@ -51,7 +56,6 @@ export async function getTeamLinks( { where: { teamId, - deletedAt: null, }, }, filters, diff --git a/src/queries/prisma/website.ts b/src/queries/prisma/website.ts index 118ec771..9d984382 100644 --- a/src/queries/prisma/website.ts +++ b/src/queries/prisma/website.ts @@ -30,10 +30,11 @@ export async function getWebsites( filters: QueryFilters, ): Promise> { const { search } = filters; + const { getSearchParameters, pagedQuery } = prisma; const where: Prisma.WebsiteWhereInput = { ...criteria.where, - ...prisma.getSearchParameters(search, [ + ...getSearchParameters(search, [ { name: 'contains', }, @@ -42,7 +43,7 @@ export async function getWebsites( deletedAt: null, }; - return prisma.pagedQuery('website', { ...criteria, where }, filters); + return pagedQuery('website', { ...criteria, where }, filters); } export async function getAllWebsites(userId: string) { diff --git a/src/validations/index.ts b/src/validations/index.ts new file mode 100644 index 00000000..d0f6b53d --- /dev/null +++ b/src/validations/index.ts @@ -0,0 +1,6 @@ +export * from './link'; +export * from './pixel'; +export * from './report'; +export * from './team'; +export * from './website'; +export * from './user'; diff --git a/src/validations/link.ts b/src/validations/link.ts new file mode 100644 index 00000000..ecead420 --- /dev/null +++ b/src/validations/link.ts @@ -0,0 +1,64 @@ +import { Auth } from '@/lib/types'; +import { getLink, getTeamUser } from '@/queries'; +import { hasPermission } from '@/lib/auth'; +import { PERMISSIONS } from '@/lib/constants'; + +export async function canViewLink({ user }: Auth, linkId: string) { + if (user?.isAdmin) { + return true; + } + + const link = await getLink(linkId); + + if (link.userId) { + return user.id === link.userId; + } + + if (link.teamId) { + const teamUser = await getTeamUser(link.teamId, user.id); + + return !!teamUser; + } + + return false; +} + +export async function canUpdateLink({ user }: Auth, linkId: string) { + if (user.isAdmin) { + return true; + } + + const link = await getLink(linkId); + + if (link.userId) { + return user.id === link.userId; + } + + if (link.teamId) { + const teamUser = await getTeamUser(link.teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteUpdate); + } + + return false; +} + +export async function canDeleteLink({ user }: Auth, linkId: string) { + if (user.isAdmin) { + return true; + } + + const link = await getLink(linkId); + + if (link.userId) { + return user.id === link.userId; + } + + if (link.teamId) { + const teamUser = await getTeamUser(link.teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteDelete); + } + + return false; +} diff --git a/src/validations/pixel.ts b/src/validations/pixel.ts new file mode 100644 index 00000000..4932bdfa --- /dev/null +++ b/src/validations/pixel.ts @@ -0,0 +1,64 @@ +import { Auth } from '@/lib/types'; +import { getPixel, getTeamUser } from '@/queries'; +import { hasPermission } from '@/lib/auth'; +import { PERMISSIONS } from '@/lib/constants'; + +export async function canViewPixel({ user }: Auth, pixelId: string) { + if (user?.isAdmin) { + return true; + } + + const pixel = await getPixel(pixelId); + + if (pixel.userId) { + return user.id === pixel.userId; + } + + if (pixel.teamId) { + const teamUser = await getTeamUser(pixel.teamId, user.id); + + return !!teamUser; + } + + return false; +} + +export async function canUpdatePixel({ user }: Auth, pixelId: string) { + if (user.isAdmin) { + return true; + } + + const pixel = await getPixel(pixelId); + + if (pixel.userId) { + return user.id === pixel.userId; + } + + if (pixel.teamId) { + const teamUser = await getTeamUser(pixel.teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteUpdate); + } + + return false; +} + +export async function canDeletePixel({ user }: Auth, pixelId: string) { + if (user.isAdmin) { + return true; + } + + const pixel = await getPixel(pixelId); + + if (pixel.userId) { + return user.id === pixel.userId; + } + + if (pixel.teamId) { + const teamUser = await getTeamUser(pixel.teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteDelete); + } + + return false; +} diff --git a/src/validations/report.ts b/src/validations/report.ts new file mode 100644 index 00000000..5895f2fc --- /dev/null +++ b/src/validations/report.ts @@ -0,0 +1,27 @@ +import { Auth } from '@/lib/types'; +import { Report } from '@prisma/client'; +import { canViewWebsite } from './website'; + +export async function canViewReport(auth: Auth, report: Report) { + if (auth.user.isAdmin) { + return true; + } + + if (auth.user.id == report.userId) { + return true; + } + + return !!(await canViewWebsite(auth, report.websiteId)); +} + +export async function canUpdateReport({ user }: Auth, report: Report) { + if (user.isAdmin) { + return true; + } + + return user.id == report.userId; +} + +export async function canDeleteReport(auth: Auth, report: Report) { + return canUpdateReport(auth, report); +} diff --git a/src/validations/team.ts b/src/validations/team.ts new file mode 100644 index 00000000..97a02b9f --- /dev/null +++ b/src/validations/team.ts @@ -0,0 +1,86 @@ +import { Auth } from '@/lib/types'; +import { PERMISSIONS } from '@/lib/constants'; +import { getTeamUser } from '@/queries'; +import { hasPermission } from '@/lib/auth'; + +const cloudMode = !!process.env.CLOUD_MODE; + +export async function canViewTeam({ user }: Auth, teamId: string) { + if (user.isAdmin) { + return true; + } + + return getTeamUser(teamId, user.id); +} + +export async function canCreateTeam({ user, grant }: Auth) { + if (cloudMode) { + return !!grant?.find(a => a === PERMISSIONS.teamCreate); + } + + if (user.isAdmin) { + return true; + } + + return !!user; +} + +export async function canUpdateTeam({ user, grant }: Auth, teamId: string) { + if (user.isAdmin) { + return true; + } + + if (cloudMode) { + return !!grant?.find(a => a === PERMISSIONS.teamUpdate); + } + + const teamUser = await getTeamUser(teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamUpdate); +} + +export async function canDeleteTeam({ user }: Auth, teamId: string) { + if (user.isAdmin) { + return true; + } + + const teamUser = await getTeamUser(teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamDelete); +} + +export async function canAddUserToTeam({ user, grant }: Auth) { + if (cloudMode) { + return !!grant?.find(a => a === PERMISSIONS.teamUpdate); + } + + return user.isAdmin; +} + +export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) { + if (user.isAdmin) { + return true; + } + + if (removeUserId === user.id) { + return true; + } + + const teamUser = await getTeamUser(teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.teamUpdate); +} + +export async function canCreateTeamWebsite({ user }: Auth, teamId: string) { + if (user.isAdmin) { + return true; + } + + const teamUser = await getTeamUser(teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteCreate); +} + +export async function canViewAllTeams({ user }: Auth) { + return user.isAdmin; +} diff --git a/src/validations/user.ts b/src/validations/user.ts new file mode 100644 index 00000000..c9a9a5f5 --- /dev/null +++ b/src/validations/user.ts @@ -0,0 +1,29 @@ +import { Auth } from '@/lib/types'; + +export async function canCreateUser({ user }: Auth) { + return user.isAdmin; +} + +export async function canViewUser({ user }: Auth, viewedUserId: string) { + if (user.isAdmin) { + return true; + } + + return user.id === viewedUserId; +} + +export async function canViewUsers({ user }: Auth) { + return user.isAdmin; +} + +export async function canUpdateUser({ user }: Auth, viewedUserId: string) { + if (user.isAdmin) { + return true; + } + + return user.id === viewedUserId; +} + +export async function canDeleteUser({ user }: Auth) { + return user.isAdmin; +} diff --git a/src/validations/website.ts b/src/validations/website.ts new file mode 100644 index 00000000..9f871b1c --- /dev/null +++ b/src/validations/website.ts @@ -0,0 +1,110 @@ +import { Auth } from '@/lib/types'; +import { PERMISSIONS } from '@/lib/constants'; +import { hasPermission } from '@/lib/auth'; +import { getTeamUser, getWebsite } from '@/queries'; + +const cloudMode = !!process.env.CLOUD_MODE; + +export async function canViewWebsite({ user, shareToken }: Auth, websiteId: string) { + if (user?.isAdmin) { + return true; + } + + if (shareToken?.websiteId === websiteId) { + return true; + } + + const website = await getWebsite(websiteId); + + if (website.userId) { + return user.id === website.userId; + } + + if (website.teamId) { + const teamUser = await getTeamUser(website.teamId, user.id); + + return !!teamUser; + } + + return false; +} + +export async function canViewAllWebsites({ user }: Auth) { + return user.isAdmin; +} + +export async function canCreateWebsite({ user, grant }: Auth) { + if (cloudMode) { + return !!grant?.find(a => a === PERMISSIONS.websiteCreate); + } + + if (user.isAdmin) { + return true; + } + + return hasPermission(user.role, PERMISSIONS.websiteCreate); +} + +export async function canUpdateWebsite({ user }: Auth, websiteId: string) { + if (user.isAdmin) { + return true; + } + + const website = await getWebsite(websiteId); + + if (website.userId) { + return user.id === website.userId; + } + + if (website.teamId) { + const teamUser = await getTeamUser(website.teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteUpdate); + } + + return false; +} + +export async function canDeleteWebsite({ user }: Auth, websiteId: string) { + if (user.isAdmin) { + return true; + } + + const website = await getWebsite(websiteId); + + if (website.userId) { + return user.id === website.userId; + } + + if (website.teamId) { + const teamUser = await getTeamUser(website.teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteDelete); + } + + return false; +} + +export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) { + const website = await getWebsite(websiteId); + + if (website.teamId && user.id === userId) { + const teamUser = await getTeamUser(website.teamId, userId); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteTransferToUser); + } + + return false; +} + +export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) { + const website = await getWebsite(websiteId); + + if (website.userId && website.userId === user.id) { + const teamUser = await getTeamUser(teamId, user.id); + + return teamUser && hasPermission(teamUser.role, PERMISSIONS.websiteTransferToTeam); + } + + return false; +}