From 9d477b25b414f593e8d6a1356ecb0e1829b62c88 Mon Sep 17 00:00:00 2001 From: Francis Cao Date: Wed, 15 Apr 2026 14:55:28 -0700 Subject: [PATCH] Fix share page permissions --- src/app/share/ShareProvider.tsx | 17 +++++++++-------- src/app/share/[slug]/[[...path]]/SharePage.tsx | 15 ++++++++++----- .../hooks/queries/useShareTokenQuery.ts | 10 +++------- src/components/hooks/useApi.ts | 12 ++++++++++-- src/lib/auth.ts | 10 +++++++++- src/lib/constants.ts | 1 + src/store/app.ts | 11 +++++------ 7 files changed, 47 insertions(+), 29 deletions(-) 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) {