diff --git a/src/app/(main)/links/[linkId]/LinkHeader.tsx b/src/app/(main)/links/[linkId]/LinkHeader.tsx index 5ca713c58..9765dea76 100644 --- a/src/app/(main)/links/[linkId]/LinkHeader.tsx +++ b/src/app/(main)/links/[linkId]/LinkHeader.tsx @@ -1,19 +1,31 @@ +import { Row } from '@umami/react-zen'; import { IconLabel } from '@/components/common/IconLabel'; import { LinkButton } from '@/components/common/LinkButton'; import { PageHeader } from '@/components/common/PageHeader'; import { useLink, useMessages, useSlug } from '@/components/hooks'; import { ExternalLink, Link } from '@/components/icons'; +import { LinkShareButton } from './LinkShareButton'; -export function LinkHeader() { - const { t, labels } = useMessages(); - const { getSlugUrl } = useSlug('link'); +export function LinkHeader({ showActions = true }: { showActions?: boolean }) { const link = useLink(); return ( }> - - } label={t(labels.view)} /> - + {showActions && link.id && } ); } + +function LinkHeaderActions({ linkId, slug }: { linkId: string; slug: string }) { + const { t, labels } = useMessages(); + const { getSlugUrl } = useSlug('link'); + + return ( + + + + } label={t(labels.view)} /> + + + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkPage.tsx b/src/app/(main)/links/[linkId]/LinkPage.tsx index ddacf08fc..5d10fa82b 100644 --- a/src/app/(main)/links/[linkId]/LinkPage.tsx +++ b/src/app/(main)/links/[linkId]/LinkPage.tsx @@ -12,13 +12,19 @@ import { Panel } from '@/components/common/Panel'; const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event']; -export function LinkPage({ linkId }: { linkId: string }) { +export function LinkPage({ + linkId, + showHeaderActions = true, +}: { + linkId: string; + showHeaderActions?: boolean; +}) { return ( - + diff --git a/src/app/(main)/links/[linkId]/LinkShareButton.tsx b/src/app/(main)/links/[linkId]/LinkShareButton.tsx new file mode 100644 index 000000000..4c3084ed5 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkShareButton.tsx @@ -0,0 +1,14 @@ +import { useMessages } from '@/components/hooks'; +import { Share } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { LinkShareDialog } from './LinkShareDialog'; + +export function LinkShareButton({ linkId }: { linkId: string }) { + const { t, labels } = useMessages(); + + return ( + } label={t(labels.share)} title={null} width="900px"> + + + ); +} diff --git a/src/app/(main)/links/[linkId]/LinkShareDialog.tsx b/src/app/(main)/links/[linkId]/LinkShareDialog.tsx new file mode 100644 index 000000000..3bf55ebf5 --- /dev/null +++ b/src/app/(main)/links/[linkId]/LinkShareDialog.tsx @@ -0,0 +1,62 @@ +import { Button, Column, Heading, Row, Text } from '@umami/react-zen'; +import { useState } from 'react'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { IconLabel } from '@/components/common/IconLabel'; +import { useLinkSharesQuery, useMessages } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm'; +import { SimpleSharesTable } from '@/components/share/SimpleSharesTable'; + +export function LinkShareDialog({ linkId }: { linkId: string }) { + const { data, error, isLoading } = useLinkSharesQuery({ linkId }); + const shares = data?.data || []; + const hasShares = shares.length > 0; + + return ( + + + + ); +} + +function LinkShareDialogContent({ + linkId, + hasShares, + shares, +}: { + linkId: string; + hasShares: boolean; + shares: any[]; +}) { + const { t, labels, messages } = useMessages(); + const [isCreating, setIsCreating] = useState(false); + const showCreateForm = !hasShares || isCreating; + + return ( + + + {t(labels.share)} + {hasShares && !isCreating && ( + + )} + + {showCreateForm && ( + setIsCreating(false)} + onCancel={hasShares ? () => setIsCreating(false) : undefined} + /> + )} + {hasShares ? ( + <> + {t(messages.shareUrl)} + + + ) : ( + !showCreateForm && {t(messages.noDataAvailable)} + )} + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx index 26afc811e..89da44483 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelHeader.tsx @@ -1,19 +1,31 @@ +import { Row } from '@umami/react-zen'; import { IconLabel } from '@/components/common/IconLabel'; import { LinkButton } from '@/components/common/LinkButton'; import { PageHeader } from '@/components/common/PageHeader'; import { useMessages, usePixel, useSlug } from '@/components/hooks'; import { ExternalLink, Grid2x2 } from '@/components/icons'; +import { PixelShareButton } from './PixelShareButton'; -export function PixelHeader() { - const { t, labels } = useMessages(); - const { getSlugUrl } = useSlug('pixel'); +export function PixelHeader({ showActions = true }: { showActions?: boolean }) { const pixel = usePixel(); return ( }> - - } label={t(labels.view)} /> - + {showActions && pixel.id && } ); } + +function PixelHeaderActions({ pixelId, slug }: { pixelId: string; slug: string }) { + const { t, labels } = useMessages(); + const { getSlugUrl } = useSlug('pixel'); + + return ( + + + + } label={t(labels.view)} /> + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx index 7a4ae9d79..e8d605d24 100644 --- a/src/app/(main)/pixels/[pixelId]/PixelPage.tsx +++ b/src/app/(main)/pixels/[pixelId]/PixelPage.tsx @@ -12,13 +12,19 @@ import { Panel } from '@/components/common/Panel'; const excludedIds = ['path', 'entry', 'exit', 'title', 'language', 'screen', 'event']; -export function PixelPage({ pixelId }: { pixelId: string }) { +export function PixelPage({ + pixelId, + showHeaderActions = true, +}: { + pixelId: string; + showHeaderActions?: boolean; +}) { return ( - + diff --git a/src/app/(main)/pixels/[pixelId]/PixelShareButton.tsx b/src/app/(main)/pixels/[pixelId]/PixelShareButton.tsx new file mode 100644 index 000000000..69ffd0adf --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelShareButton.tsx @@ -0,0 +1,14 @@ +import { useMessages } from '@/components/hooks'; +import { Share } from '@/components/icons'; +import { DialogButton } from '@/components/input/DialogButton'; +import { PixelShareDialog } from './PixelShareDialog'; + +export function PixelShareButton({ pixelId }: { pixelId: string }) { + const { t, labels } = useMessages(); + + return ( + } label={t(labels.share)} title={null} width="900px"> + + + ); +} diff --git a/src/app/(main)/pixels/[pixelId]/PixelShareDialog.tsx b/src/app/(main)/pixels/[pixelId]/PixelShareDialog.tsx new file mode 100644 index 000000000..58d6b37ca --- /dev/null +++ b/src/app/(main)/pixels/[pixelId]/PixelShareDialog.tsx @@ -0,0 +1,62 @@ +import { Button, Column, Heading, Row, Text } from '@umami/react-zen'; +import { useState } from 'react'; +import { LoadingPanel } from '@/components/common/LoadingPanel'; +import { IconLabel } from '@/components/common/IconLabel'; +import { useMessages, usePixelSharesQuery } from '@/components/hooks'; +import { Plus } from '@/components/icons'; +import { SimpleShareCreateForm } from '@/components/share/SimpleShareCreateForm'; +import { SimpleSharesTable } from '@/components/share/SimpleSharesTable'; + +export function PixelShareDialog({ pixelId }: { pixelId: string }) { + const { data, error, isLoading } = usePixelSharesQuery({ pixelId }); + const shares = data?.data || []; + const hasShares = shares.length > 0; + + return ( + + + + ); +} + +function PixelShareDialogContent({ + pixelId, + hasShares, + shares, +}: { + pixelId: string; + hasShares: boolean; + shares: any[]; +}) { + const { t, labels, messages } = useMessages(); + const [isCreating, setIsCreating] = useState(false); + const showCreateForm = !hasShares || isCreating; + + return ( + + + {t(labels.share)} + {hasShares && !isCreating && ( + + )} + + {showCreateForm && ( + setIsCreating(false)} + onCancel={hasShares ? () => setIsCreating(false) : undefined} + /> + )} + {hasShares ? ( + <> + {t(messages.shareUrl)} + + + ) : ( + !showCreateForm && {t(messages.noDataAvailable)} + )} + + ); +} diff --git a/src/app/api/links/[linkId]/shares/route.ts b/src/app/api/links/[linkId]/shares/route.ts new file mode 100644 index 000000000..0ed8516e6 --- /dev/null +++ b/src/app/api/links/[linkId]/shares/route.ts @@ -0,0 +1,73 @@ +import { z } from 'zod'; +import { ENTITY_TYPE } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { getRandomChars } from '@/lib/generate'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, pagingParams } from '@/lib/schema'; +import { canUpdateLink, canViewLink } from '@/permissions'; +import { createShare, getSharesByEntityId } from '@/queries/prisma'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ linkId: string }> }, +) { + const schema = z.object({ + ...filterParams, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { linkId } = await params; + const { page, pageSize, search } = query; + + if (!(await canViewLink(auth, linkId))) { + return unauthorized(); + } + + const data = await getSharesByEntityId(linkId, { + page, + pageSize, + search, + }); + + return json(data); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ linkId: string }> }, +) { + const schema = z.object({ + name: z.string().max(200), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { linkId } = await params; + const { name } = body; + + if (!(await canUpdateLink(auth, linkId))) { + return unauthorized(); + } + + const share = await createShare({ + id: uuid(), + entityId: linkId, + shareType: ENTITY_TYPE.link, + name, + slug: getRandomChars(16), + parameters: {}, + }); + + return json(share); +} diff --git a/src/app/api/pixels/[pixelId]/shares/route.ts b/src/app/api/pixels/[pixelId]/shares/route.ts new file mode 100644 index 000000000..8b688cd7c --- /dev/null +++ b/src/app/api/pixels/[pixelId]/shares/route.ts @@ -0,0 +1,73 @@ +import { z } from 'zod'; +import { ENTITY_TYPE } from '@/lib/constants'; +import { uuid } from '@/lib/crypto'; +import { getRandomChars } from '@/lib/generate'; +import { parseRequest } from '@/lib/request'; +import { json, unauthorized } from '@/lib/response'; +import { filterParams, pagingParams } from '@/lib/schema'; +import { canUpdatePixel, canViewPixel } from '@/permissions'; +import { createShare, getSharesByEntityId } from '@/queries/prisma'; + +export async function GET( + request: Request, + { params }: { params: Promise<{ pixelId: string }> }, +) { + const schema = z.object({ + ...filterParams, + ...pagingParams, + }); + + const { auth, query, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { pixelId } = await params; + const { page, pageSize, search } = query; + + if (!(await canViewPixel(auth, pixelId))) { + return unauthorized(); + } + + const data = await getSharesByEntityId(pixelId, { + page, + pageSize, + search, + }); + + return json(data); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ pixelId: string }> }, +) { + const schema = z.object({ + name: z.string().max(200), + }); + + const { auth, body, error } = await parseRequest(request, schema); + + if (error) { + return error(); + } + + const { pixelId } = await params; + const { name } = body; + + if (!(await canUpdatePixel(auth, pixelId))) { + return unauthorized(); + } + + const share = await createShare({ + id: uuid(), + entityId: pixelId, + shareType: ENTITY_TYPE.pixel, + name, + slug: getRandomChars(16), + parameters: {}, + }); + + return json(share); +} diff --git a/src/app/api/share/[slug]/route.ts b/src/app/api/share/[slug]/route.ts index fa7f99343..a237a8f17 100644 --- a/src/app/api/share/[slug]/route.ts +++ b/src/app/api/share/[slug]/route.ts @@ -6,17 +6,17 @@ import prisma from '@/lib/prisma'; import redis from '@/lib/redis'; import { json, notFound } from '@/lib/response'; import type { WhiteLabel } from '@/lib/types'; -import { getBoard, getShareByCode, getWebsite } from '@/queries/prisma'; +import { getBoard, getLink, getPixel, getShareByCode, getWebsite } from '@/queries/prisma'; -async function getAccountId(website: { userId?: string; teamId?: string }): Promise { - if (website.userId) { - return website.userId; +async function getAccountId(entity: { userId?: string; teamId?: string }): Promise { + if (entity.userId) { + return entity.userId; } - if (website.teamId) { + if (entity.teamId) { const teamOwner = await prisma.client.teamUser.findFirst({ where: { - teamId: website.teamId, + teamId: entity.teamId, role: ROLES.teamOwner, }, select: { @@ -73,22 +73,46 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu return json(data); } - const website = await getWebsite(share.entityId); - - if (!website) { - return notFound(); - } - + let entity: { userId?: string; teamId?: string } | null = null; const data: Record = { shareId: share.id, shareType: share.shareType, - websiteId: share.entityId, parameters: share.parameters, }; + if (share.shareType === ENTITY_TYPE.website) { + entity = await getWebsite(share.entityId); + + if (!entity) { + return notFound(); + } + + data.websiteId = share.entityId; + } else if (share.shareType === ENTITY_TYPE.pixel) { + entity = await getPixel(share.entityId); + + if (!entity) { + return notFound(); + } + + data.websiteId = share.entityId; + data.pixelId = share.entityId; + } else if (share.shareType === ENTITY_TYPE.link) { + entity = await getLink(share.entityId); + + if (!entity) { + return notFound(); + } + + data.websiteId = share.entityId; + data.linkId = share.entityId; + } else { + return notFound(); + } + data.token = createToken(data, secret()); - const accountId = await getAccountId(website); + const accountId = await getAccountId(entity); if (accountId) { const whiteLabel = await getWhiteLabel(accountId); diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx index 4d148d6a6..a09a3776e 100644 --- a/src/app/share/ShareProvider.tsx +++ b/src/app/share/ShareProvider.tsx @@ -13,6 +13,8 @@ export interface ShareData { websiteId?: string; websiteIds?: string[]; boardId?: string; + pixelId?: string; + linkId?: string; parameters: any; token: string; whiteLabel?: WhiteLabel; @@ -54,12 +56,13 @@ export function ShareProvider({ slug, children }: { slug: string; children: Reac const pathname = usePathname(); const path = getSharePath(pathname); const isBoardShare = share?.shareType === ENTITY_TYPE.board; + const isWebsiteShare = share?.shareType === ENTITY_TYPE.website; - const allowedSections = !isBoardShare && share?.parameters + const allowedSections = isWebsiteShare && share?.parameters ? ALL_SECTION_IDS.filter(id => share.parameters[id] !== false) : []; - const shouldRedirect = !isBoardShare && + const shouldRedirect = isWebsiteShare && allowedSections.length === 1 && allowedSections[0] !== 'overview' && (path === undefined || path === '' || path === 'overview'); diff --git a/src/app/share/[slug]/[[...path]]/SharePage.tsx b/src/app/share/[slug]/[[...path]]/SharePage.tsx index 83a85317d..88b8967a9 100644 --- a/src/app/share/[slug]/[[...path]]/SharePage.tsx +++ b/src/app/share/[slug]/[[...path]]/SharePage.tsx @@ -2,6 +2,8 @@ import { Column, Grid, Row, useTheme } from '@umami/react-zen'; import { usePathname } from 'next/navigation'; import { useEffect, useState } from 'react'; +import { LinkPage } from '@/app/(main)/links/[linkId]/LinkPage'; +import { PixelPage } from '@/app/(main)/pixels/[pixelId]/PixelPage'; import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage'; import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage'; import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage'; @@ -59,7 +61,7 @@ export function SharePage() { const { setTheme } = useTheme(); const pathname = usePathname(); const path = getSharePath(pathname); - const { websiteId, boardId, parameters = {}, shareType } = share; + const { websiteId, boardId, pixelId, linkId, parameters = {}, shareType } = share; useEffect(() => { const url = new URL(window?.location?.href); @@ -81,6 +83,14 @@ export function SharePage() { ); } + if (shareType === ENTITY_TYPE.pixel && pixelId) { + return ; + } + + if (shareType === ENTITY_TYPE.link && linkId) { + return ; + } + // Check if the requested path is allowed const pageKey = path || ''; const isAllowed = pageKey === '' || pageKey === 'overview' || parameters[pageKey] !== false; diff --git a/src/components/hooks/index.ts b/src/components/hooks/index.ts index 2d844edf4..5a1d17422 100644 --- a/src/components/hooks/index.ts +++ b/src/components/hooks/index.ts @@ -22,9 +22,11 @@ export * from './queries/useEventDataPropertiesQuery'; export * from './queries/useEventDataQuery'; export * from './queries/useEventDataValuesQuery'; export * from './queries/useLinkQuery'; +export * from './queries/useLinkSharesQuery'; export * from './queries/useLinksQuery'; export * from './queries/useLoginQuery'; export * from './queries/usePixelQuery'; +export * from './queries/usePixelSharesQuery'; export * from './queries/usePixelsQuery'; export * from './queries/useRealtimeQuery'; export * from './queries/useReplayQuery'; diff --git a/src/components/hooks/queries/useLinkSharesQuery.ts b/src/components/hooks/queries/useLinkSharesQuery.ts new file mode 100644 index 000000000..990952d05 --- /dev/null +++ b/src/components/hooks/queries/useLinkSharesQuery.ts @@ -0,0 +1,20 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useModified } from '../useModified'; +import { usePagedQuery } from '../usePagedQuery'; + +export function useLinkSharesQuery( + { linkId }: { linkId: string }, + options?: ReactQueryOptions, +) { + const { modified } = useModified('shares'); + const { get } = useApi(); + + return usePagedQuery({ + queryKey: ['linkShares', { linkId, modified }], + queryFn: pageParams => { + return get(`/links/${linkId}/shares`, pageParams); + }, + ...options, + }); +} diff --git a/src/components/hooks/queries/usePixelSharesQuery.ts b/src/components/hooks/queries/usePixelSharesQuery.ts new file mode 100644 index 000000000..aacd07a7a --- /dev/null +++ b/src/components/hooks/queries/usePixelSharesQuery.ts @@ -0,0 +1,20 @@ +import type { ReactQueryOptions } from '@/lib/types'; +import { useApi } from '../useApi'; +import { useModified } from '../useModified'; +import { usePagedQuery } from '../usePagedQuery'; + +export function usePixelSharesQuery( + { pixelId }: { pixelId: string }, + options?: ReactQueryOptions, +) { + const { modified } = useModified('shares'); + const { get } = useApi(); + + return usePagedQuery({ + queryKey: ['pixelShares', { pixelId, modified }], + queryFn: pageParams => { + return get(`/pixels/${pixelId}/shares`, pageParams); + }, + ...options, + }); +} diff --git a/src/components/hooks/useSlug.ts b/src/components/hooks/useSlug.ts index f795dfeb3..b4f585af0 100644 --- a/src/components/hooks/useSlug.ts +++ b/src/components/hooks/useSlug.ts @@ -2,7 +2,9 @@ import { useConfig } from '@/components/hooks/useConfig'; import { LINKS_URL, PIXELS_URL } from '@/lib/constants'; export function useSlug(type: 'link' | 'pixel') { - const { linksUrl, pixelsUrl } = useConfig(); + const config = useConfig(); + const linksUrl = config?.linksUrl; + const pixelsUrl = config?.pixelsUrl; const hostUrl = type === 'link' ? linksUrl || LINKS_URL : pixelsUrl || PIXELS_URL; diff --git a/src/components/share/SimpleShareCreateForm.tsx b/src/components/share/SimpleShareCreateForm.tsx new file mode 100644 index 000000000..9ab850bcb --- /dev/null +++ b/src/components/share/SimpleShareCreateForm.tsx @@ -0,0 +1,57 @@ +import { Button, Column, Form, FormField, FormSubmitButton, Row, TextField } from '@umami/react-zen'; +import { useState } from 'react'; +import { useApi, useMessages, useModified } from '@/components/hooks'; + +export function SimpleShareCreateForm({ + createPath, + onSave, + onCancel, +}: { + createPath: string; + onSave?: () => void; + onCancel?: () => void; +}) { + const { post } = useApi(); + const { touch } = useModified(); + const { t, labels, getErrorMessage } = useMessages(); + const [isPending, setIsPending] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = async (data: { name: string }) => { + setIsPending(true); + setError(null); + + try { + await post(createPath, { + name: data.name, + }); + + touch('shares'); + onSave?.(); + } catch (e) { + setError(e); + } finally { + setIsPending(false); + } + }; + + return ( +
+ + + + + + {onCancel && ( + + )} + + {t(labels.add)} + + + +
+ ); +} diff --git a/src/components/share/SimpleSharesTable.tsx b/src/components/share/SimpleSharesTable.tsx new file mode 100644 index 000000000..b938e6412 --- /dev/null +++ b/src/components/share/SimpleSharesTable.tsx @@ -0,0 +1,52 @@ +import { DataColumn, DataTable, type DataTableProps, Row } from '@umami/react-zen'; +import { ShareDeleteButton } from '@/app/(main)/websites/[websiteId]/settings/ShareDeleteButton'; +import { CopyButton } from '@/components/common/CopyButton'; +import { DateDistance } from '@/components/common/DateDistance'; +import { ExternalLink } from '@/components/common/ExternalLink'; +import { useConfig, useMessages, useMobile } from '@/components/hooks'; + +export function SimpleSharesTable(props: DataTableProps) { + const { t, labels } = useMessages(); + const { cloudMode } = useConfig(); + const { isMobile } = useMobile(); + + const getUrl = (slug: string) => { + if (cloudMode) { + return `${process.env.cloudUrl}/share/${slug}`; + } + + return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`; + }; + + return ( + + + {({ name }: any) => name} + + + {({ slug }: any) => { + const url = getUrl(slug); + + return ( + + + {isMobile ? slug : url} + + + + ); + }} + + + {(row: any) => } + + + {({ id, slug }: any) => ( + + + + )} + + + ); +} diff --git a/src/lib/types.ts b/src/lib/types.ts index 7b174d9f5..f2e49dab6 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -23,6 +23,8 @@ export interface Auth { websiteId?: string; websiteIds?: string[]; boardId?: string; + pixelId?: string; + linkId?: string; }; } diff --git a/src/permissions/link.ts b/src/permissions/link.ts index 8dd1d7c6b..e77676998 100644 --- a/src/permissions/link.ts +++ b/src/permissions/link.ts @@ -3,15 +3,19 @@ import { PERMISSIONS } from '@/lib/constants'; import type { Auth } from '@/lib/types'; import { getLink, getTeamUser } from '@/queries/prisma'; -export async function canViewLink({ user }: Auth, linkId: string) { - if (!user) { - return false; +export async function canViewLink({ user, shareToken }: Auth, linkId: string) { + if (user?.isAdmin) { + return true; } - if (user.isAdmin) { + if (shareToken?.linkId === linkId || shareToken?.websiteId === linkId) { return true; } + if (!user) { + return false; + } + const link = await getLink(linkId); if (link.userId) { diff --git a/src/permissions/pixel.ts b/src/permissions/pixel.ts index 14b69acef..bf7c7aed0 100644 --- a/src/permissions/pixel.ts +++ b/src/permissions/pixel.ts @@ -3,15 +3,19 @@ import { PERMISSIONS } from '@/lib/constants'; import type { Auth } from '@/lib/types'; import { getPixel, getTeamUser } from '@/queries/prisma'; -export async function canViewPixel({ user }: Auth, pixelId: string) { - if (!user) { - return false; +export async function canViewPixel({ user, shareToken }: Auth, pixelId: string) { + if (user?.isAdmin) { + return true; } - if (user.isAdmin) { + if (shareToken?.pixelId === pixelId || shareToken?.websiteId === pixelId) { return true; } + if (!user) { + return false; + } + const pixel = await getPixel(pixelId); if (pixel.userId) { diff --git a/src/permissions/website.ts b/src/permissions/website.ts index 69d8c6f2d..7d4efe1d3 100644 --- a/src/permissions/website.ts +++ b/src/permissions/website.ts @@ -9,7 +9,12 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri return true; } - if (shareToken?.websiteId === websiteId || shareToken?.websiteIds?.includes(websiteId)) { + if ( + shareToken?.websiteId === websiteId || + shareToken?.pixelId === websiteId || + shareToken?.linkId === websiteId || + shareToken?.websiteIds?.includes(websiteId) + ) { return true; }