mirror of
https://github.com/umami-software/umami.git
synced 2026-02-12 08:37:13 +01:00
Compare commits
No commits in common. "c5aa8be15cbee74f3264bcb09437f6cde12b857b" and "67cdfdfb7e7ee3fea9a2699155df8dfbd826f724" have entirely different histories.
c5aa8be15c
...
67cdfdfb7e
30 changed files with 285 additions and 503 deletions
94
.gitignore
vendored
94
.gitignore
vendored
|
|
@ -1,48 +1,46 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store
|
||||
package-lock.json
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next
|
||||
/out
|
||||
|
||||
# production
|
||||
/build
|
||||
/public/script.js
|
||||
/geo
|
||||
/dist
|
||||
/generated
|
||||
/src/generated
|
||||
pm2.yml
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.idea
|
||||
.yarn
|
||||
*.iml
|
||||
*.log
|
||||
.vscode
|
||||
.tool-versions
|
||||
.claude
|
||||
nul
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.*
|
||||
*.env.*
|
||||
|
||||
*.dev.yml
|
||||
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
.pnp
|
||||
.pnp.js
|
||||
.pnpm-store
|
||||
package-lock.json
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next
|
||||
/out
|
||||
|
||||
# production
|
||||
/build
|
||||
/public/script.js
|
||||
/geo
|
||||
/dist
|
||||
/generated
|
||||
/src/generated
|
||||
pm2.yml
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.idea
|
||||
.yarn
|
||||
*.iml
|
||||
*.log
|
||||
.vscode
|
||||
.tool-versions
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.*
|
||||
*.env.*
|
||||
|
||||
*.dev.yml
|
||||
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
},
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev -p 3002 --turbo",
|
||||
"dev": "next dev -p 3001 --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",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Box, Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
||||
import { useMessages, useResultQuery } from '@/components/hooks';
|
||||
import { File, User } from '@/components/icons';
|
||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||
import { ChangeLabel } from '@/components/metrics/ChangeLabel';
|
||||
|
|
@ -20,8 +20,6 @@ type FunnelResult = {
|
|||
|
||||
export function Funnel({ id, name, type, parameters, websiteId }) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
const { data, error, isLoading } = useResultQuery(type, {
|
||||
websiteId,
|
||||
...parameters,
|
||||
|
|
@ -38,22 +36,21 @@ export function Funnel({ id, name, type, parameters, websiteId }) {
|
|||
</Text>
|
||||
</Row>
|
||||
</Column>
|
||||
{!isSharePage && (
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.funnel)}
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
)}
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.funnel)}
|
||||
variant="modal"
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<FunnelEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
</Grid>
|
||||
{data?.map(
|
||||
(
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
|||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
||||
import { Funnel } from './Funnel';
|
||||
import { FunnelAddButton } from './FunnelAddButton';
|
||||
|
||||
|
|
@ -13,17 +13,13 @@ export function FunnelsPage({ websiteId }: { websiteId: string }) {
|
|||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
{!isSharePage && (
|
||||
<SectionHeader>
|
||||
<FunnelAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
)}
|
||||
<SectionHeader>
|
||||
<FunnelAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
<Grid gap>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { Column, Dialog, Grid, Icon, ProgressBar, Row, Text } from '@umami/react-zen';
|
||||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { useMessages, useNavigation, useResultQuery } from '@/components/hooks';
|
||||
import { useMessages, useResultQuery } from '@/components/hooks';
|
||||
import { File, User } from '@/components/icons';
|
||||
import { ReportEditButton } from '@/components/input/ReportEditButton';
|
||||
import { Lightning } from '@/components/svg';
|
||||
|
|
@ -25,8 +25,6 @@ export type GoalData = { num: number; total: number };
|
|||
|
||||
export function Goal({ id, name, type, parameters, websiteId, startDate, endDate }: GoalProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
const { data, error, isLoading, isFetching } = useResultQuery<GoalData>(type, {
|
||||
websiteId,
|
||||
startDate,
|
||||
|
|
@ -47,23 +45,21 @@ export function Goal({ id, name, type, parameters, websiteId, startDate, endDate
|
|||
</Text>
|
||||
</Row>
|
||||
</Column>
|
||||
{!isSharePage && (
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.goal)}
|
||||
variant="modal"
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
)}
|
||||
<Column>
|
||||
<ReportEditButton id={id} name={name} type={type}>
|
||||
{({ close }) => {
|
||||
return (
|
||||
<Dialog
|
||||
title={formatMessage(labels.goal)}
|
||||
variant="modal"
|
||||
style={{ minHeight: 300, minWidth: 400 }}
|
||||
>
|
||||
<GoalEditForm id={id} websiteId={websiteId} onClose={close} />
|
||||
</Dialog>
|
||||
);
|
||||
}}
|
||||
</ReportEditButton>
|
||||
</Column>
|
||||
</Grid>
|
||||
<Row alignItems="center" justifyContent="space-between" gap>
|
||||
<Text color="muted">
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { WebsiteControls } from '@/app/(main)/websites/[websiteId]/WebsiteContro
|
|||
import { LoadingPanel } from '@/components/common/LoadingPanel';
|
||||
import { Panel } from '@/components/common/Panel';
|
||||
import { SectionHeader } from '@/components/common/SectionHeader';
|
||||
import { useDateRange, useNavigation, useReportsQuery } from '@/components/hooks';
|
||||
import { useDateRange, useReportsQuery } from '@/components/hooks';
|
||||
import { Goal } from './Goal';
|
||||
import { GoalAddButton } from './GoalAddButton';
|
||||
|
||||
|
|
@ -13,17 +13,13 @@ export function GoalsPage({ websiteId }: { websiteId: string }) {
|
|||
const {
|
||||
dateRange: { startDate, endDate },
|
||||
} = useDateRange();
|
||||
const { pathname } = useNavigation();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
|
||||
return (
|
||||
<Column gap>
|
||||
<WebsiteControls websiteId={websiteId} />
|
||||
{!isSharePage && (
|
||||
<SectionHeader>
|
||||
<GoalAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
)}
|
||||
<SectionHeader>
|
||||
<GoalAddButton websiteId={websiteId} />
|
||||
</SectionHeader>
|
||||
<LoadingPanel data={data} isLoading={isLoading} error={error}>
|
||||
{data && (
|
||||
<Grid columns={{ xs: '1fr', md: '1fr 1fr' }} gap>
|
||||
|
|
|
|||
|
|
@ -6,13 +6,7 @@ import { useMessages, useNavigation, useWebsite } from '@/components/hooks';
|
|||
import { Edit } from '@/components/icons';
|
||||
import { ActiveUsers } from '@/components/metrics/ActiveUsers';
|
||||
|
||||
export function WebsiteHeader({
|
||||
showActions,
|
||||
allowLink = true,
|
||||
}: {
|
||||
showActions?: boolean;
|
||||
allowLink?: boolean;
|
||||
}) {
|
||||
export function WebsiteHeader({ showActions }: { showActions?: boolean }) {
|
||||
const website = useWebsite();
|
||||
const { renderUrl, pathname } = useNavigation();
|
||||
const isSettings = pathname.endsWith('/settings');
|
||||
|
|
@ -27,7 +21,7 @@ export function WebsiteHeader({
|
|||
<PageHeader
|
||||
title={website.name}
|
||||
icon={<Favicon domain={website.domain} />}
|
||||
titleHref={allowLink ? renderUrl(`/websites/${website.id}`, false) : undefined}
|
||||
titleHref={renderUrl(`/websites/${website.id}`, false)}
|
||||
>
|
||||
<Row alignItems="center" gap="6" wrap="wrap">
|
||||
<ActiveUsers websiteId={website.id} />
|
||||
|
|
|
|||
|
|
@ -0,0 +1,81 @@
|
|||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Column,
|
||||
Form,
|
||||
FormField,
|
||||
FormSubmitButton,
|
||||
Row,
|
||||
Text,
|
||||
} from '@umami/react-zen';
|
||||
import { useState } from 'react';
|
||||
import { useApi, useMessages, useModified } from '@/components/hooks';
|
||||
import { SHARE_NAV_ITEMS } from './constants';
|
||||
|
||||
export interface ShareCreateFormProps {
|
||||
websiteId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export function ShareCreateForm({ websiteId, onSave, onClose }: ShareCreateFormProps) {
|
||||
const { formatMessage, labels } = useMessages();
|
||||
const { post } = useApi();
|
||||
const { touch } = useModified();
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
|
||||
// Build default values - only overview and events enabled by default
|
||||
const defaultValues: Record<string, boolean> = {};
|
||||
SHARE_NAV_ITEMS.forEach(section => {
|
||||
section.items.forEach(item => {
|
||||
defaultValues[item.id] = item.id === 'overview' || item.id === 'events';
|
||||
});
|
||||
});
|
||||
|
||||
const handleSubmit = async (data: any) => {
|
||||
setIsPending(true);
|
||||
try {
|
||||
const parameters: Record<string, boolean> = {};
|
||||
SHARE_NAV_ITEMS.forEach(section => {
|
||||
section.items.forEach(item => {
|
||||
parameters[item.id] = data[item.id] ?? false;
|
||||
});
|
||||
});
|
||||
await post(`/websites/${websiteId}/shares`, { parameters });
|
||||
touch('shares');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} defaultValues={defaultValues}>
|
||||
<Column gap="3">
|
||||
{SHARE_NAV_ITEMS.map(section => (
|
||||
<Column key={section.section} gap="1">
|
||||
<Text size="2" weight="bold">
|
||||
{formatMessage((labels as any)[section.section])}
|
||||
</Text>
|
||||
<Column gap="1">
|
||||
{section.items.map(item => (
|
||||
<FormField key={item.id} name={item.id}>
|
||||
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
||||
</FormField>
|
||||
))}
|
||||
</Column>
|
||||
</Column>
|
||||
))}
|
||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||
{onClose && (
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
<FormSubmitButton isDisabled={isPending}>{formatMessage(labels.save)}</FormSubmitButton>
|
||||
</Row>
|
||||
</Column>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
@ -5,7 +5,6 @@ import {
|
|||
Form,
|
||||
FormField,
|
||||
FormSubmitButton,
|
||||
Grid,
|
||||
Label,
|
||||
Loading,
|
||||
Row,
|
||||
|
|
@ -14,30 +13,25 @@ import {
|
|||
} from '@umami/react-zen';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useApi, useConfig, useMessages, useModified } from '@/components/hooks';
|
||||
import { useUpdateQuery } from '@/components/hooks/queries/useUpdateQuery';
|
||||
import { SHARE_NAV_ITEMS } from './constants';
|
||||
|
||||
export function ShareEditForm({
|
||||
shareId,
|
||||
websiteId,
|
||||
onSave,
|
||||
onClose,
|
||||
}: {
|
||||
shareId?: string;
|
||||
websiteId?: string;
|
||||
shareId: string;
|
||||
onSave?: () => void;
|
||||
onClose?: () => void;
|
||||
}) {
|
||||
const { formatMessage, labels, getErrorMessage } = useMessages();
|
||||
const { formatMessage, labels, messages, getErrorMessage } = useMessages();
|
||||
const { mutateAsync, error, isPending, touch, toast } = useUpdateQuery(`/share/id/${shareId}`);
|
||||
const { cloudMode } = useConfig();
|
||||
const { get, post } = useApi();
|
||||
const { touch } = useModified();
|
||||
const { get } = useApi();
|
||||
const { modified } = useModified('shares');
|
||||
const [share, setShare] = useState<any>(null);
|
||||
const [isLoading, setIsLoading] = useState(!!shareId);
|
||||
const [isPending, setIsPending] = useState(false);
|
||||
const [error, setError] = useState<any>(null);
|
||||
|
||||
const isEditing = !!shareId;
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const getUrl = (slug: string) => {
|
||||
if (cloudMode) {
|
||||
|
|
@ -47,8 +41,6 @@ export function ShareEditForm({
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!shareId) return;
|
||||
|
||||
const loadShare = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
|
|
@ -69,35 +61,27 @@ export function ShareEditForm({
|
|||
});
|
||||
});
|
||||
|
||||
setIsPending(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
if (isEditing) {
|
||||
await post(`/share/id/${shareId}`, { name: data.name, slug: share.slug, parameters });
|
||||
} else {
|
||||
await post(`/websites/${websiteId}/shares`, { name: data.name, parameters });
|
||||
}
|
||||
touch('shares');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
} finally {
|
||||
setIsPending(false);
|
||||
}
|
||||
await mutateAsync(
|
||||
{ slug: share.slug, parameters },
|
||||
{
|
||||
onSuccess: async () => {
|
||||
toast(formatMessage(messages.saved));
|
||||
touch('shares');
|
||||
onSave?.();
|
||||
onClose?.();
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Loading placement="absolute" />;
|
||||
}
|
||||
|
||||
const url = isEditing ? getUrl(share?.slug || '') : null;
|
||||
const url = getUrl(share?.slug || '');
|
||||
|
||||
// Build default values from share parameters
|
||||
const defaultValues: Record<string, any> = {
|
||||
name: share?.name || '',
|
||||
};
|
||||
const defaultValues: Record<string, boolean> = {};
|
||||
SHARE_NAV_ITEMS.forEach(section => {
|
||||
section.items.forEach(item => {
|
||||
const defaultSelected = item.id === 'overview' || item.id === 'events';
|
||||
|
|
@ -105,60 +89,34 @@ export function ShareEditForm({
|
|||
});
|
||||
});
|
||||
|
||||
// Get all item ids for validation
|
||||
const allItemIds = SHARE_NAV_ITEMS.flatMap(section => section.items.map(item => item.id));
|
||||
|
||||
return (
|
||||
<Form onSubmit={handleSubmit} error={getErrorMessage(error)} defaultValues={defaultValues}>
|
||||
{({ watch }) => {
|
||||
const values = watch();
|
||||
const hasSelection = allItemIds.some(id => values[id]);
|
||||
|
||||
return (
|
||||
<Column gap="6">
|
||||
{url && (
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
</Column>
|
||||
)}
|
||||
<FormField
|
||||
label={formatMessage(labels.name)}
|
||||
name="name"
|
||||
rules={{ required: formatMessage(labels.required) }}
|
||||
>
|
||||
<TextField autoComplete="off" autoFocus={!isEditing} />
|
||||
</FormField>
|
||||
<Grid columns="repeat(auto-fit, minmax(150px, 1fr))" gap="3">
|
||||
{SHARE_NAV_ITEMS.map(section => (
|
||||
<Column key={section.section} gap="3">
|
||||
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
|
||||
<Column gap="1">
|
||||
{section.items.map(item => (
|
||||
<FormField key={item.id} name={item.id}>
|
||||
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
||||
</FormField>
|
||||
))}
|
||||
</Column>
|
||||
</Column>
|
||||
<Column gap="3">
|
||||
<Column>
|
||||
<Label>{formatMessage(labels.shareUrl)}</Label>
|
||||
<TextField value={url} isReadOnly allowCopy />
|
||||
</Column>
|
||||
{SHARE_NAV_ITEMS.map(section => (
|
||||
<Column key={section.section} gap="1">
|
||||
<Text weight="bold">{formatMessage((labels as any)[section.section])}</Text>
|
||||
<Column gap="1">
|
||||
{section.items.map(item => (
|
||||
<FormField key={item.id} name={item.id}>
|
||||
<Checkbox>{formatMessage((labels as any)[item.label])}</Checkbox>
|
||||
</FormField>
|
||||
))}
|
||||
</Grid>
|
||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||
{onClose && (
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
<FormSubmitButton
|
||||
variant="primary"
|
||||
isDisabled={isPending || !hasSelection || !values.name}
|
||||
>
|
||||
{formatMessage(labels.save)}
|
||||
</FormSubmitButton>
|
||||
</Row>
|
||||
</Column>
|
||||
</Column>
|
||||
);
|
||||
}}
|
||||
))}
|
||||
<Row justifyContent="flex-end" paddingTop="3" gap="3">
|
||||
{onClose && (
|
||||
<Button isDisabled={isPending} onPress={onClose}>
|
||||
{formatMessage(labels.cancel)}
|
||||
</Button>
|
||||
)}
|
||||
<FormSubmitButton variant="primary">{formatMessage(labels.save)}</FormSubmitButton>
|
||||
</Row>
|
||||
</Column>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,14 +10,14 @@ export function SharesTable(props: DataTableProps) {
|
|||
const { cloudMode } = useConfig();
|
||||
|
||||
const getUrl = (slug: string) => {
|
||||
return `${cloudMode ? process.env.cloudUrl : window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
||||
if (cloudMode) {
|
||||
return `${process.env.cloudUrl}/share/${slug}`;
|
||||
}
|
||||
return `${window?.location.origin}${process.env.basePath || ''}/share/${slug}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<DataTable {...props}>
|
||||
<DataColumn id="name" label={formatMessage(labels.name)}>
|
||||
{({ name }: any) => name}
|
||||
</DataColumn>
|
||||
<DataColumn id="slug" label={formatMessage(labels.shareUrl)}>
|
||||
{({ slug }: any) => {
|
||||
const url = getUrl(slug);
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Column, Heading, Row, Text } from '@umami/react-zen';
|
|||
import { Plus } from 'lucide-react';
|
||||
import { useMessages, useWebsiteSharesQuery } from '@/components/hooks';
|
||||
import { DialogButton } from '@/components/input/DialogButton';
|
||||
import { ShareEditForm } from './ShareEditForm';
|
||||
import { ShareCreateForm } from './ShareCreateForm';
|
||||
import { SharesTable } from './SharesTable';
|
||||
|
||||
export interface WebsiteShareFormProps {
|
||||
|
|
@ -25,9 +25,9 @@ export function WebsiteShareForm({ websiteId }: WebsiteShareFormProps) {
|
|||
label={formatMessage(labels.add)}
|
||||
title={formatMessage(labels.share)}
|
||||
variant="primary"
|
||||
width="600px"
|
||||
width="400px"
|
||||
>
|
||||
{({ close }) => <ShareEditForm websiteId={websiteId} onClose={close} />}
|
||||
{({ close }) => <ShareCreateForm websiteId={websiteId} onClose={close} />}
|
||||
</DialogButton>
|
||||
</Row>
|
||||
{hasShares ? (
|
||||
|
|
|
|||
|
|
@ -1,47 +1,7 @@
|
|||
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 type { WhiteLabel } from '@/lib/types';
|
||||
import { getShareByCode, getWebsite } from '@/queries/prisma';
|
||||
|
||||
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;
|
||||
}
|
||||
import { getShareByCode } from '@/queries/prisma';
|
||||
|
||||
export async function GET(_request: Request, { params }: { params: Promise<{ slug: string }> }) {
|
||||
const { slug } = await params;
|
||||
|
|
@ -52,25 +12,12 @@ export async function GET(_request: Request, { params }: { params: Promise<{ slu
|
|||
return notFound();
|
||||
}
|
||||
|
||||
const website = await getWebsite(share.entityId);
|
||||
|
||||
const data: Record<string, any> = {
|
||||
const data = {
|
||||
shareId: share.id,
|
||||
websiteId: share.entityId,
|
||||
parameters: share.parameters,
|
||||
};
|
||||
const token = createToken(data, secret());
|
||||
|
||||
data.token = createToken(data, secret());
|
||||
|
||||
const accountId = await getAccountId(website);
|
||||
|
||||
if (accountId) {
|
||||
const whiteLabel = await getWhiteLabel(accountId);
|
||||
|
||||
if (whiteLabel) {
|
||||
data.whiteLabel = whiteLabel;
|
||||
}
|
||||
}
|
||||
|
||||
return json(data);
|
||||
return json({ ...data, token });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ export async function GET(request: Request, { params }: { params: Promise<{ shar
|
|||
|
||||
export async function POST(request: Request, { params }: { params: Promise<{ shareId: string }> }) {
|
||||
const schema = z.object({
|
||||
name: z.string().max(200),
|
||||
slug: z.string().max(100),
|
||||
parameters: anyObjectParam,
|
||||
});
|
||||
|
|
@ -37,7 +36,7 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
|
|||
}
|
||||
|
||||
const { shareId } = await params;
|
||||
const { name, slug, parameters } = body;
|
||||
const { slug, parameters } = body;
|
||||
|
||||
const share = await getShare(shareId);
|
||||
|
||||
|
|
@ -50,7 +49,6 @@ export async function POST(request: Request, { params }: { params: Promise<{ sha
|
|||
}
|
||||
|
||||
const result = await updateShare(shareId, {
|
||||
name,
|
||||
slug,
|
||||
parameters,
|
||||
} as any);
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ export async function POST(
|
|||
{ params }: { params: Promise<{ websiteId: string }> },
|
||||
) {
|
||||
const schema = z.object({
|
||||
name: z.string().max(200),
|
||||
parameters: anyObjectParam.optional(),
|
||||
});
|
||||
|
||||
|
|
@ -55,8 +54,7 @@ export async function POST(
|
|||
}
|
||||
|
||||
const { websiteId } = await params;
|
||||
const { name, parameters } = body;
|
||||
const shareParameters = parameters ?? {};
|
||||
const { parameters = {} } = body;
|
||||
|
||||
if (!(await canUpdateWebsite(auth, websiteId))) {
|
||||
return unauthorized();
|
||||
|
|
@ -68,9 +66,8 @@ export async function POST(
|
|||
id: uuid(),
|
||||
entityId: websiteId,
|
||||
shareType: ENTITY_TYPE.website,
|
||||
name,
|
||||
slug,
|
||||
parameters: shareParameters,
|
||||
parameters,
|
||||
});
|
||||
|
||||
return json(share);
|
||||
|
|
|
|||
12
src/app/share/[...shareId]/Footer.tsx
Normal file
12
src/app/share/[...shareId]/Footer.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Row, Text } from '@umami/react-zen';
|
||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
||||
<a href={HOMEPAGE_URL} target="_blank">
|
||||
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
||||
</a>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
24
src/app/share/[...shareId]/Header.tsx
Normal file
24
src/app/share/[...shareId]/Header.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
||||
import { Logo } from '@/components/svg';
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
||||
<a href="https://umami.is" target="_blank" rel="noopener">
|
||||
<Row alignItems="center" gap>
|
||||
<Icon>
|
||||
<Logo />
|
||||
</Icon>
|
||||
<Text weight="bold">umami</Text>
|
||||
</Row>
|
||||
</a>
|
||||
<Row alignItems="center" gap>
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<PreferencesButton />
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { Row, Text } from '@umami/react-zen';
|
||||
import { CURRENT_VERSION, HOMEPAGE_URL } from '@/lib/constants';
|
||||
import type { WhiteLabel } from '@/lib/types';
|
||||
|
||||
export function ShareFooter({ 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 (
|
||||
<Row as="footer" paddingY="6" justifyContent="flex-end">
|
||||
<a href={HOMEPAGE_URL} target="_blank">
|
||||
<Text weight="bold">umami</Text> {`v${CURRENT_VERSION}`}
|
||||
</a>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { Icon, Row, Text, ThemeButton } from '@umami/react-zen';
|
||||
import { LanguageButton } from '@/components/input/LanguageButton';
|
||||
import { PreferencesButton } from '@/components/input/PreferencesButton';
|
||||
import { Logo } from '@/components/svg';
|
||||
import type { WhiteLabel } from '@/lib/types';
|
||||
|
||||
export function ShareHeader({ whiteLabel }: { whiteLabel?: WhiteLabel }) {
|
||||
const logoUrl = whiteLabel?.url || 'https://umami.is';
|
||||
const logoName = whiteLabel?.name || 'umami';
|
||||
const logoImage = whiteLabel?.image;
|
||||
|
||||
return (
|
||||
<Row as="header" justifyContent="space-between" alignItems="center" paddingY="3">
|
||||
<a href={logoUrl} target="_blank" rel="noopener">
|
||||
<Row alignItems="center" gap>
|
||||
{logoImage ? (
|
||||
<img src={logoImage} alt={logoName} style={{ height: 24 }} />
|
||||
) : (
|
||||
<Icon>
|
||||
<Logo />
|
||||
</Icon>
|
||||
)}
|
||||
<Text weight="bold">{logoName}</Text>
|
||||
</Row>
|
||||
</a>
|
||||
<Row alignItems="center" gap>
|
||||
<ThemeButton />
|
||||
<LanguageButton />
|
||||
<PreferencesButton />
|
||||
</Row>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
'use client';
|
||||
import { Column, Grid, Row, useTheme } from '@umami/react-zen';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { Column, Grid, useTheme } from '@umami/react-zen';
|
||||
import { useEffect } from 'react';
|
||||
import { AttributionPage } from '@/app/(main)/websites/[websiteId]/(reports)/attribution/AttributionPage';
|
||||
import { BreakdownPage } from '@/app/(main)/websites/[websiteId]/(reports)/breakdown/BreakdownPage';
|
||||
import { FunnelsPage } from '@/app/(main)/websites/[websiteId]/(reports)/funnels/FunnelsPage';
|
||||
|
|
@ -19,9 +18,8 @@ import { WebsitePage } from '@/app/(main)/websites/[websiteId]/WebsitePage';
|
|||
import { WebsiteProvider } from '@/app/(main)/websites/WebsiteProvider';
|
||||
import { PageBody } from '@/components/common/PageBody';
|
||||
import { useShareTokenQuery } from '@/components/hooks';
|
||||
import { MobileMenuButton } from '@/components/input/MobileMenuButton';
|
||||
import { ShareFooter } from './ShareFooter';
|
||||
import { ShareHeader } from './ShareHeader';
|
||||
import { Footer } from './Footer';
|
||||
import { Header } from './Header';
|
||||
import { ShareNav } from './ShareNav';
|
||||
|
||||
const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>> = {
|
||||
|
|
@ -41,34 +39,9 @@ const PAGE_COMPONENTS: Record<string, React.ComponentType<{ websiteId: string }>
|
|||
attribution: AttributionPage,
|
||||
};
|
||||
|
||||
// All section IDs that can be enabled/disabled via parameters
|
||||
const ALL_SECTION_IDS = [
|
||||
'overview',
|
||||
'events',
|
||||
'sessions',
|
||||
'realtime',
|
||||
'compare',
|
||||
'breakdown',
|
||||
'goals',
|
||||
'funnels',
|
||||
'journeys',
|
||||
'retention',
|
||||
'utm',
|
||||
'revenue',
|
||||
'attribution',
|
||||
];
|
||||
|
||||
export function SharePage({ shareId, path = '' }: { shareId: string; path?: string }) {
|
||||
const { shareToken, isLoading } = useShareTokenQuery(shareId);
|
||||
const { setTheme } = useTheme();
|
||||
const router = useRouter();
|
||||
|
||||
// Calculate allowed sections
|
||||
const allowedSections = useMemo(() => {
|
||||
if (!shareToken?.parameters) return [];
|
||||
const params = shareToken.parameters;
|
||||
return ALL_SECTION_IDS.filter(id => params[id] !== false);
|
||||
}, [shareToken?.parameters]);
|
||||
|
||||
useEffect(() => {
|
||||
const url = new URL(window?.location?.href);
|
||||
|
|
@ -79,31 +52,11 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
|||
}
|
||||
}, []);
|
||||
|
||||
// Redirect to the only allowed section if there's just one and we're on the base path
|
||||
useEffect(() => {
|
||||
if (
|
||||
allowedSections.length === 1 &&
|
||||
allowedSections[0] !== 'overview' &&
|
||||
(path === '' || path === 'overview')
|
||||
) {
|
||||
router.replace(`/share/${shareId}/${allowedSections[0]}`);
|
||||
}
|
||||
}, [allowedSections, shareId, path, router]);
|
||||
|
||||
if (isLoading || !shareToken) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { websiteId, parameters = {}, whiteLabel } = shareToken;
|
||||
|
||||
// Redirect to only allowed section - return null while redirecting
|
||||
if (
|
||||
allowedSections.length === 1 &&
|
||||
allowedSections[0] !== 'overview' &&
|
||||
(path === '' || path === 'overview')
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
const { websiteId, parameters = {} } = shareToken;
|
||||
|
||||
// Check if the requested path is allowed
|
||||
const pageKey = path || '';
|
||||
|
|
@ -117,16 +70,8 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
|||
|
||||
return (
|
||||
<Column backgroundColor="2">
|
||||
<Header />
|
||||
<Grid columns={{ xs: '1fr', lg: 'auto 1fr' }} width="100%" height="100%">
|
||||
<Row display={{ xs: 'flex', lg: 'none' }} alignItems="center" gap padding="3">
|
||||
<Grid columns="auto 1fr" flexGrow={1} backgroundColor="3" borderRadius>
|
||||
<MobileMenuButton>
|
||||
{({ close }) => {
|
||||
return <ShareNav shareId={shareId} parameters={parameters} onItemClick={close} />;
|
||||
}}
|
||||
</MobileMenuButton>
|
||||
</Grid>
|
||||
</Row>
|
||||
<Column
|
||||
display={{ xs: 'none', lg: 'flex' }}
|
||||
width="240px"
|
||||
|
|
@ -135,21 +80,18 @@ export function SharePage({ shareId, path = '' }: { shareId: string; path?: stri
|
|||
backgroundColor
|
||||
marginRight="2"
|
||||
>
|
||||
<Column display={{ xs: 'none', lg: 'flex' }}>
|
||||
<ShareNav shareId={shareId} parameters={parameters} />
|
||||
</Column>
|
||||
<ShareNav shareId={shareId} parameters={parameters} />
|
||||
</Column>
|
||||
<PageBody gap>
|
||||
<WebsiteProvider websiteId={websiteId}>
|
||||
<ShareHeader whiteLabel={whiteLabel} />
|
||||
<WebsiteHeader showActions={false} />
|
||||
<Column>
|
||||
<WebsiteHeader showActions={false} />
|
||||
<PageComponent websiteId={websiteId} />
|
||||
</Column>
|
||||
<ShareFooter whiteLabel={whiteLabel} />
|
||||
</WebsiteProvider>
|
||||
</PageBody>
|
||||
</Grid>
|
||||
<Footer />
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ export function PageBody({
|
|||
<Column
|
||||
{...props}
|
||||
width="100%"
|
||||
minHeight="100vh"
|
||||
paddingBottom="6"
|
||||
maxWidth={maxWidth}
|
||||
paddingX={{ xs: '3', md: '6' }}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export function WebsiteSelect({
|
|||
const { user } = useLoginQuery();
|
||||
const { data, isLoading } = useUserWebsitesQuery(
|
||||
{ userId: user?.id, teamId },
|
||||
{ search, pageSize: 20, includeTeams },
|
||||
{ search, pageSize: 10, includeTeams },
|
||||
);
|
||||
const listItems: { id: string; name: string }[] = data?.data || [];
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export function renderDateLabels(unit: string, locale: string) {
|
|||
|
||||
switch (unit) {
|
||||
case 'minute':
|
||||
return formatDate(d, 'h:mm', locale);
|
||||
case 'hour':
|
||||
return formatDate(d, 'p', locale);
|
||||
case 'day':
|
||||
|
|
|
|||
|
|
@ -141,9 +141,3 @@ export interface ApiError extends Error {
|
|||
code?: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface WhiteLabel {
|
||||
name: string;
|
||||
url: string;
|
||||
image: string;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,11 +5,7 @@ import type { Auth } from '@/lib/types';
|
|||
import { getTeamUser } from '@/queries/prisma';
|
||||
|
||||
export async function canViewEntity({ user }: Auth, entityId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
if (user?.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -29,10 +25,6 @@ export async function canViewEntity({ user }: Auth, entityId: string) {
|
|||
}
|
||||
|
||||
export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -53,10 +45,6 @@ export async function canUpdateEntity({ user }: Auth, entityId: string) {
|
|||
}
|
||||
|
||||
export async function canDeleteEntity({ user }: Auth, entityId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ import type { Auth } from '@/lib/types';
|
|||
import { getLink, getTeamUser } from '@/queries/prisma';
|
||||
|
||||
export async function canViewLink({ user }: Auth, linkId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
if (user?.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -28,10 +24,6 @@ export async function canViewLink({ user }: Auth, linkId: string) {
|
|||
}
|
||||
|
||||
export async function canUpdateLink({ user }: Auth, linkId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -52,10 +44,6 @@ export async function canUpdateLink({ user }: Auth, linkId: string) {
|
|||
}
|
||||
|
||||
export async function canDeleteLink({ user }: Auth, linkId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,7 @@ import type { Auth } from '@/lib/types';
|
|||
import { getPixel, getTeamUser } from '@/queries/prisma';
|
||||
|
||||
export async function canViewPixel({ user }: Auth, pixelId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
if (user?.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -28,10 +24,6 @@ export async function canViewPixel({ user }: Auth, pixelId: string) {
|
|||
}
|
||||
|
||||
export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -52,10 +44,6 @@ export async function canUpdatePixel({ user }: Auth, pixelId: string) {
|
|||
}
|
||||
|
||||
export async function canDeletePixel({ user }: Auth, pixelId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import type { Auth } from '@/lib/types';
|
|||
import { canViewWebsite } from './website';
|
||||
|
||||
export async function canViewReport(auth: Auth, report: Report) {
|
||||
if (auth.user?.isAdmin) {
|
||||
if (auth.user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (auth.user?.id === report.userId) {
|
||||
if (auth.user.id === report.userId) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -15,10 +15,6 @@ export async function canViewReport(auth: Auth, report: Report) {
|
|||
}
|
||||
|
||||
export async function canUpdateReport({ user }: Auth, report: Report) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,10 +4,6 @@ import type { Auth } from '@/lib/types';
|
|||
import { getTeamUser } from '@/queries/prisma';
|
||||
|
||||
export async function canViewTeam({ user }: Auth, teamId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -16,10 +12,6 @@ export async function canViewTeam({ user }: Auth, teamId: string) {
|
|||
}
|
||||
|
||||
export async function canCreateTeam({ user }: Auth) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -28,10 +20,6 @@ export async function canCreateTeam({ user }: Auth) {
|
|||
}
|
||||
|
||||
export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -42,10 +30,6 @@ export async function canUpdateTeam({ user }: Auth, teamId: string) {
|
|||
}
|
||||
|
||||
export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -56,10 +40,6 @@ export async function canDeleteTeam({ user }: Auth, teamId: string) {
|
|||
}
|
||||
|
||||
export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUserId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -74,10 +54,6 @@ export async function canDeleteTeamUser({ user }: Auth, teamId: string, removeUs
|
|||
}
|
||||
|
||||
export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -88,5 +64,5 @@ export async function canCreateTeamWebsite({ user }: Auth, teamId: string) {
|
|||
}
|
||||
|
||||
export async function canViewAllTeams({ user }: Auth) {
|
||||
return user?.isAdmin ?? false;
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,10 @@
|
|||
import type { Auth } from '@/lib/types';
|
||||
|
||||
export async function canCreateUser({ user }: Auth) {
|
||||
return user?.isAdmin ?? false;
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
||||
export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -17,14 +13,10 @@ export async function canViewUser({ user }: Auth, viewedUserId: string) {
|
|||
}
|
||||
|
||||
export async function canViewUsers({ user }: Auth) {
|
||||
return user?.isAdmin ?? false;
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
||||
export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -33,5 +25,5 @@ export async function canUpdateUser({ user }: Auth, viewedUserId: string) {
|
|||
}
|
||||
|
||||
export async function canDeleteUser({ user }: Auth) {
|
||||
return user?.isAdmin ?? false;
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
|
|||
|
||||
const entity = await getEntity(websiteId);
|
||||
|
||||
if (!entity || !user) {
|
||||
if (!entity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
@ -33,14 +33,10 @@ export async function canViewWebsite({ user, shareToken }: Auth, websiteId: stri
|
|||
}
|
||||
|
||||
export async function canViewAllWebsites({ user }: Auth) {
|
||||
return user?.isAdmin ?? false;
|
||||
return user.isAdmin;
|
||||
}
|
||||
|
||||
export async function canCreateWebsite({ user }: Auth) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -49,10 +45,6 @@ export async function canCreateWebsite({ user }: Auth) {
|
|||
}
|
||||
|
||||
export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -77,10 +69,6 @@ export async function canUpdateWebsite({ user }: Auth, websiteId: string) {
|
|||
}
|
||||
|
||||
export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user.isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
|
@ -105,10 +93,6 @@ export async function canDeleteWebsite({ user }: Auth, websiteId: string) {
|
|||
}
|
||||
|
||||
export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string, userId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const website = await getWebsite(websiteId);
|
||||
|
||||
if (!website) {
|
||||
|
|
@ -125,10 +109,6 @@ export async function canTransferWebsiteToUser({ user }: Auth, websiteId: string
|
|||
}
|
||||
|
||||
export async function canTransferWebsiteToTeam({ user }: Auth, websiteId: string, teamId: string) {
|
||||
if (!user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const website = await getWebsite(websiteId);
|
||||
|
||||
if (!website) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue