diff --git a/src/app/share/ShareProvider.tsx b/src/app/share/ShareProvider.tsx
index 60e989014..ab01690eb 100644
--- a/src/app/share/ShareProvider.tsx
+++ b/src/app/share/ShareProvider.tsx
@@ -1,10 +1,10 @@
'use client';
-import { Loading } from '@umami/react-zen';
-import { usePathname, useRouter } from 'next/navigation';
-import { createContext, type ReactNode, useEffect } from 'react';
import { useShareTokenQuery } from '@/components/hooks';
import { ENTITY_TYPE } from '@/lib/constants';
import type { WhiteLabel } from '@/lib/types';
+import { Loading } from '@umami/react-zen';
+import { usePathname, useRouter } from 'next/navigation';
+import { createContext, type ReactNode, useEffect } from 'react';
export interface ShareData {
shareId: string;
@@ -56,14 +56,15 @@ export function ShareProvider({ slug, children }: { slug: string; children: Reac
const router = useRouter();
const pathname = usePathname();
const path = getSharePath(pathname);
- const isBoardShare = share?.shareType === ENTITY_TYPE.board;
const isWebsiteShare = share?.shareType === ENTITY_TYPE.website;
- const allowedSections = isWebsiteShare && share?.parameters
- ? ALL_SECTION_IDS.filter(id => share.parameters[id] === true)
- : [];
+ const allowedSections =
+ isWebsiteShare && share?.parameters
+ ? ALL_SECTION_IDS.filter(id => share.parameters[id] === true)
+ : [];
- const shouldRedirect = isWebsiteShare &&
+ 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 750097aa1..122ad0a0b 100644
--- a/src/app/share/[slug]/[[...path]]/SharePage.tsx
+++ b/src/app/share/[slug]/[[...path]]/SharePage.tsx
@@ -83,6 +83,10 @@ export function SharePage() {
}
}, [setTheme]);
+ // Check if the requested path is allowed
+ const pageKey = path || '';
+ const isAllowed = pageKey === '' || parameters[pageKey] === true;
+
const entityPage =
shareType === ENTITY_TYPE.board && boardId ? (
@@ -92,6 +96,12 @@ export function SharePage() {
) : null;
+ useEffect(() => {
+ if (!isAllowed) {
+ router.replace(`/share/${slug}`);
+ }
+ }, [isAllowed, slug, router]);
+
if (entityPage) {
return (
@@ -101,12 +111,7 @@ export function SharePage() {
);
}
- // Check if the requested path is allowed
- const pageKey = path || '';
- const isAllowed = pageKey === '' || parameters[pageKey] === true;
-
if (!isAllowed) {
- router.replace(`/share/${slug}`);
return null;
}
diff --git a/src/components/hooks/queries/useShareTokenQuery.ts b/src/components/hooks/queries/useShareTokenQuery.ts
index 17d9e885a..362fe8ee3 100644
--- a/src/components/hooks/queries/useShareTokenQuery.ts
+++ b/src/components/hooks/queries/useShareTokenQuery.ts
@@ -1,22 +1,18 @@
-import { setShare, setShareToken, useApp } from '@/store/app';
+import { setShareData } from '@/store/app';
import { useApi } from '../useApi';
-const selector = state => state.share;
-
export function useShareTokenQuery(slug: string) {
- const share = useApp(selector);
const { get, useQuery } = useApi();
const query = useQuery({
queryKey: ['share', slug],
queryFn: async () => {
const data = await get(`/share/${slug}`);
- setShare(data);
- setShareToken({ token: data?.token });
+ setShareData(data, { token: data?.token });
return data;
},
});
- return { share, ...query };
+ return { share: query.data, ...query };
}
diff --git a/src/components/hooks/useApi.ts b/src/components/hooks/useApi.ts
index 35cabd529..d89e6f18b 100644
--- a/src/components/hooks/useApi.ts
+++ b/src/components/hooks/useApi.ts
@@ -1,7 +1,8 @@
import { useMutation, useQuery } from '@tanstack/react-query';
+import { usePathname } from 'next/navigation';
import { useCallback } from 'react';
import { getClientAuthToken } from '@/lib/client';
-import { SHARE_TOKEN_HEADER } from '@/lib/constants';
+import { SHARE_CONTEXT_HEADER, SHARE_TOKEN_HEADER } from '@/lib/constants';
import { type FetchResponse, httpDelete, httpGet, httpPost, httpPut } from '@/lib/fetch';
import { useApp } from '@/store/app';
@@ -18,10 +19,17 @@ async function handleResponse(res: FetchResponse): Promise {
export function useApi() {
const shareToken = useApp(selector);
+ const pathname = usePathname();
+ const isSharePath = pathname?.startsWith('/share');
+
+ const shareHeaders =
+ isSharePath && shareToken?.token
+ ? { [SHARE_TOKEN_HEADER]: shareToken.token, [SHARE_CONTEXT_HEADER]: '1' }
+ : {};
const defaultHeaders = {
authorization: `Bearer ${getClientAuthToken()}`,
- [SHARE_TOKEN_HEADER]: shareToken?.token,
+ ...shareHeaders,
};
const basePath = process.env.basePath;
diff --git a/src/lib/auth.ts b/src/lib/auth.ts
index 832dfb600..49de033ec 100644
--- a/src/lib/auth.ts
+++ b/src/lib/auth.ts
@@ -1,5 +1,5 @@
import debug from 'debug';
-import { ROLE_PERMISSIONS, ROLES, SHARE_TOKEN_HEADER } from '@/lib/constants';
+import { ROLE_PERMISSIONS, ROLES, SHARE_CONTEXT_HEADER, SHARE_TOKEN_HEADER } from '@/lib/constants';
import { createAuthKey, secret } from '@/lib/crypto';
import { createSecureToken, parseSecureToken, parseToken } from '@/lib/jwt';
import redis from '@/lib/redis';
@@ -39,6 +39,14 @@ export async function checkAuth(request: Request) {
return null;
}
+ if (!user?.id && shareToken) {
+ const shareContext = request.headers.get(SHARE_CONTEXT_HEADER);
+ if (!shareContext) {
+ log('Share token used outside share context');
+ return null;
+ }
+ }
+
if (user) {
user.isAdmin = user.role === ROLES.admin;
}
diff --git a/src/lib/constants.ts b/src/lib/constants.ts
index a4a004f31..f7c182b15 100644
--- a/src/lib/constants.ts
+++ b/src/lib/constants.ts
@@ -9,6 +9,7 @@ export const DASHBOARD_CONFIG = 'umami.dashboard';
export const LAST_TEAM_CONFIG = 'umami.last-team';
export const VERSION_CHECK = 'umami.version-check';
export const SHARE_TOKEN_HEADER = 'x-umami-share-token';
+export const SHARE_CONTEXT_HEADER = 'x-umami-share-context';
export const HOMEPAGE_URL = 'https://umami.is';
export const DOCS_URL = 'https://umami.is/docs';
export const REPO_URL = 'https://github.com/umami-software/umami';
diff --git a/src/store/app.ts b/src/store/app.ts
index e8600174d..0893c506a 100644
--- a/src/store/app.ts
+++ b/src/store/app.ts
@@ -32,12 +32,11 @@ export function setLocale(locale: string) {
store.setState({ locale });
}
-export function setShare(share: object) {
- store.setState({ share });
-}
-
-export function setShareToken(shareToken: { token?: string } | null) {
- store.setState({ shareToken });
+export function setShareData(
+ share: object | null,
+ shareToken: { token?: string } | null,
+) {
+ store.setState({ share, shareToken });
}
export function setUser(user: object) {