Fix share page permissions

This commit is contained in:
Francis Cao
2026-04-15 14:55:28 -07:00
parent f41c0221c4
commit 9d477b25b4
7 changed files with 47 additions and 29 deletions
+9 -8
View File
@@ -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');
+10 -5
View File
@@ -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 ? (
<BoardViewPage boardId={boardId} showActions={false} />
@@ -92,6 +96,12 @@ export function SharePage() {
<LinkPage linkId={linkId} showHeaderActions={false} />
) : null;
useEffect(() => {
if (!isAllowed) {
router.replace(`/share/${slug}`);
}
}, [isAllowed, slug, router]);
if (entityPage) {
return (
<Column>
@@ -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;
}
@@ -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 };
}
+10 -2
View File
@@ -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<any> {
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;
+9 -1
View File
@@ -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;
}
+1
View File
@@ -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';
+5 -6
View File
@@ -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) {