mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Compare commits
2 commits
52d9dd2871
...
518f0b66c6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
518f0b66c6 | ||
|
|
f84e67b0e6 |
7 changed files with 102 additions and 16 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -30,6 +30,7 @@ package-lock.json
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
.tool-versions
|
.tool-versions
|
||||||
|
.claude
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"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",
|
"build": "npm-run-all check-env build-db check-db build-tracker build-geo build-app",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,13 @@ import { Edit, Share } from '@/components/icons';
|
||||||
import { DialogButton } from '@/components/input/DialogButton';
|
import { DialogButton } from '@/components/input/DialogButton';
|
||||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
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 website = useWebsite();
|
||||||
const { renderUrl, pathname } = useNavigation();
|
const { renderUrl, pathname } = useNavigation();
|
||||||
const isSettings = pathname.endsWith('/settings');
|
const isSettings = pathname.endsWith('/settings');
|
||||||
|
|
@ -23,7 +29,7 @@ export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title={website.name}
|
title={website.name}
|
||||||
icon={<Favicon domain={website.domain} />}
|
icon={<Favicon domain={website.domain} />}
|
||||||
titleHref={renderUrl(`/websites/${website.id}`, false)}
|
titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
|
||||||
>
|
>
|
||||||
<Row alignItems="center" gap="6" wrap="wrap">
|
<Row alignItems="center" gap="6" wrap="wrap">
|
||||||
<ActiveUsers websiteId={website.id} />
|
<ActiveUsers websiteId={website.id} />
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,53 @@
|
||||||
|
import { ROLES } from '@/lib/constants';
|
||||||
import { secret } from '@/lib/crypto';
|
import { secret } from '@/lib/crypto';
|
||||||
import { createToken } from '@/lib/jwt';
|
import { createToken } from '@/lib/jwt';
|
||||||
|
import prisma from '@/lib/prisma';
|
||||||
|
import redis from '@/lib/redis';
|
||||||
import { json, notFound } from '@/lib/response';
|
import { json, notFound } from '@/lib/response';
|
||||||
import { getSharedWebsite } from '@/queries/prisma';
|
import { getSharedWebsite } from '@/queries/prisma';
|
||||||
|
|
||||||
|
export interface WhiteLabel {
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
image: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getAccountId(website: { userId?: string; teamId?: string }): Promise<string | null> {
|
||||||
|
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<WhiteLabel | null> {
|
||||||
|
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 }> }) {
|
export async function GET(_request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
||||||
const { shareId } = await params;
|
const { shareId } = await params;
|
||||||
|
|
||||||
|
|
@ -12,8 +57,20 @@ export async function GET(_request: Request, { params }: { params: Promise<{ sha
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = { websiteId: website.id };
|
const data: { websiteId: string; token: string; whiteLabel?: WhiteLabel } = {
|
||||||
const token = createToken(data, secret());
|
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,18 @@
|
||||||
import { Row, Text } from '@umami/react-zen';
|
import { Row, Text } from '@umami/react-zen';
|
||||||
|
import type { WhiteLabel } from '@/app/api/share/[shareId]/route';
|
||||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
||||||
|
|
||||||
export function Footer() {
|
export function Footer({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
||||||
|
if (whiteLabel) {
|
||||||
|
return (
|
||||||
|
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
||||||
|
<a href={whiteLabel.url} target="_blank">
|
||||||
|
<Text weight="bold">{whiteLabel.name}</Text>
|
||||||
|
</a>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
||||||
<a href={HOMEPAGE_URL} target="_blank">
|
<a href={HOMEPAGE_URL} target="_blank">
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,26 @@
|
||||||
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
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 { LanguageButton } from '@/components/input/LanguageButton';
|
||||||
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
||||||
import { Logo } from '@/components/svg';
|
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 (
|
return (
|
||||||
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
||||||
<a href="https://umami.is" target="_blank" rel="noopener">
|
<a href={logoUrl} target="_blank" rel="noopener">
|
||||||
<Row alignItems="center" gap>
|
<Row alignItems="center" gap>
|
||||||
<Icon>
|
{logoImage ? (
|
||||||
<Logo />
|
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
||||||
</Icon>
|
) : (
|
||||||
<Text weight="bold">umami</Text>
|
<Icon>
|
||||||
|
<Logo />
|
||||||
|
</Icon>
|
||||||
|
)}
|
||||||
|
<Text weight="bold">{logoName}</Text>
|
||||||
</Row>
|
</Row>
|
||||||
</a>
|
</a>
|
||||||
<Row alignItems="center" gap>
|
<Row alignItems="center" gap>
|
||||||
|
|
|
||||||
|
|
@ -26,15 +26,17 @@ export function SharePage({ shareId }) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { whiteLabel } = shareToken;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Column backgroundColor="2">
|
<Column backgroundColor="2">
|
||||||
<PageBody gap>
|
<PageBody gap>
|
||||||
<Header />
|
<Header whiteLabel={whiteLabel} />
|
||||||
<WebsiteProvider websiteId={shareToken.websiteId}>
|
<WebsiteProvider websiteId={shareToken.websiteId}>
|
||||||
<WebsiteHeader showActions={false} />
|
<WebsiteHeader showActions={false} allowLink={false} />
|
||||||
<WebsitePage websiteId={shareToken.websiteId} />
|
<WebsitePage websiteId={shareToken.websiteId} />
|
||||||
</WebsiteProvider>
|
</WebsiteProvider>
|
||||||
<Footer />
|
<Footer whiteLabel={whiteLabel} />
|
||||||
</PageBody>
|
</PageBody>
|
||||||
</Column>
|
</Column>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue