diff --git a/.gitignore b/.gitignore index 753389d1..64cdb30d 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ package-lock.json *.log .vscode .tool-versions +.claude # debug npm-debug.log* diff --git a/package.json b/package.json index 09a69135..2c0279aa 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ }, "type": "module", "scripts": { - "dev": "next dev -p 3001 --turbo", + "dev": "next dev -p 3003 --turbo", "build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app", "start": "next start", "build-docker": "npm-run-all build-db build-tracker build-geo build-app", diff --git a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx index 7dd1d771..0d8b5d28 100644 --- a/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx +++ b/src/app/(main)/websites/[websiteId]/WebsiteHeader.tsx @@ -8,7 +8,13 @@ import { Edit, Share } from '@/components/icons'; import { DialogButton } from '@/components/input/DialogButton'; import { ActiveUsers } from '@/components/metrics/ActiveUsers'; -export function WebsiteHeader({ showActions }: { showActions?: boolean }) { +export function WebsiteHeader({ + showActions, + allowLink = true, +}: { + showActions?: boolean; + allowLink?: boolean; +}) { const website = useWebsite(); const { renderUrl, pathname } = useNavigation(); const isSettings = pathname.endsWith('/settings'); @@ -23,7 +29,7 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) { } - titleHref={renderUrl(`/websites/${website.id}`, false)} + titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined} > diff --git a/src/app/api/share/[shareId]/route.ts b/src/app/api/share/[shareId]/route.ts index bef87c4f..8b0c9bb9 100644 --- a/src/app/api/share/[shareId]/route.ts +++ b/src/app/api/share/[shareId]/route.ts @@ -1,8 +1,53 @@ +import { ROLES } from '@/lib/constants'; import { secret } from '@/lib/crypto'; import { createToken } from '@/lib/jwt'; +import prisma from '@/lib/prisma'; +import redis from '@/lib/redis'; import { json, notFound } from '@/lib/response'; import { getSharedWebsite } from '@/queries/prisma'; +export interface WhiteLabel { + name: string; + url: string; + image: string; +} + +async function getAccountId(website: { userId?: string; teamId?: string }): Promise { + if (website.userId) { + return website.userId; + } + + if (website.teamId) { + const teamOwner = await prisma.client.teamUser.findFirst({ + where: { + teamId: website.teamId, + role: ROLES.teamOwner, + }, + select: { + userId: true, + }, + }); + + return teamOwner?.userId || null; + } + + return null; +} + +async function getWhiteLabel(accountId: string): Promise { + if (!redis.enabled) { + return null; + } + + const data = await redis.client.get(`white-label:${accountId}`); + + if (data) { + return data as WhiteLabel; + } + + return null; +} + export async function GET(_request: Request, { params }: { params: Promise<{ shareId: string }> }) { const { shareId } = await params; @@ -12,8 +57,20 @@ export async function GET(_request: Request, { params }: { params: Promise<{ sha return notFound(); } - const data = { websiteId: website.id }; - const token = createToken(data, secret()); + const data: { websiteId: string; token: string; whiteLabel?: WhiteLabel } = { + websiteId: website.id, + token: createToken({ websiteId: website.id }, secret()), + }; - return json({ ...data, token }); + const accountId = await getAccountId(website); + + if (accountId) { + const whiteLabel = await getWhiteLabel(accountId); + + if (whiteLabel) { + data.whiteLabel = whiteLabel; + } + } + + return json(data); } diff --git a/src/app/share/[...shareId]/Footer.tsx b/src/app/share/[...shareId]/Footer.tsx index f2948628..0f17187c 100644 --- a/src/app/share/[...shareId]/Footer.tsx +++ b/src/app/share/[...shareId]/Footer.tsx @@ -1,7 +1,18 @@ import { Row, Text } from '@umami/react-zen'; +import type { WhiteLabel } from '@/app/api/share/[shareId]/route'; import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants'; -export function Footer() { +export function Footer({ whiteLabel }: { whiteLabel?: WhiteLabel }) { + if (whiteLabel) { + return ( + + + {whiteLabel.name} + + + ); + } + return ( diff --git a/src/app/share/[...shareId]/Header.tsx b/src/app/share/[...shareId]/Header.tsx index d7b7dcb4..78e022af 100644 --- a/src/app/share/[...shareId]/Header.tsx +++ b/src/app/share/[...shareId]/Header.tsx @@ -1,17 +1,26 @@ import { Icon, Row, Text, ThemeButton } from '@umami/react-zen'; +import type { WhiteLabel } from '@/app/api/share/[shareId]/route'; import { LanguageButton } from '@/components/input/LanguageButton'; import { PreferencesButton } from '@/components/input/PreferencesButton'; import { Logo } from '@/components/svg'; -export function Header() { +export function Header({ whiteLabel }: { whiteLabel?: WhiteLabel }) { + const logoUrl = whiteLabel?.url || 'https://umami.is'; + const logoName = whiteLabel?.name || 'umami'; + const logoImage = whiteLabel?.image; + return ( - + - - - - umami + {logoImage ? ( + {logoName} + ) : ( + + + + )} + {logoName} diff --git a/src/app/share/[...shareId]/SharePage.tsx b/src/app/share/[...shareId]/SharePage.tsx index 7ed06673..7a76a29b 100644 --- a/src/app/share/[...shareId]/SharePage.tsx +++ b/src/app/share/[...shareId]/SharePage.tsx @@ -26,15 +26,17 @@ export function SharePage({ shareId }) { return null; } + const { whiteLabel } = shareToken; + return ( -
+
- + -