Use token authentication for API requests.

This commit is contained in:
Mike Cao 2020-09-17 22:52:20 -07:00
parent bff8806b61
commit 96bd7e5b47
34 changed files with 198 additions and 153 deletions

View File

@ -30,16 +30,19 @@ const views = {
event: EventsTable,
};
export default function WebsiteDetails({ websiteId, shareId }) {
export default function WebsiteDetails({ websiteId, token }) {
const router = useRouter();
const { data } = useFetch(`/api/website/${websiteId}`, { share_id: shareId });
const { data } = useFetch(`/api/website/${websiteId}`, { token });
const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
const {
query: { id, view },
basePath,
asPath,
} = router;
const path = `/website/${id.join('/')}`;
const path = `${basePath}/${asPath.split('/')[1]}/${id.join('/')}`;
const BackButton = () => (
<Button
@ -91,6 +94,7 @@ export default function WebsiteDetails({ websiteId, shareId }) {
const tableProps = {
websiteId,
token,
websiteDomain: data?.domain,
limit: 10,
onExpand: handleExpand,
@ -118,6 +122,7 @@ export default function WebsiteDetails({ websiteId, shareId }) {
<div className={classNames(styles.chart, 'col')}>
<WebsiteChart
websiteId={websiteId}
token={token}
title={data.name}
onDataLoad={handleDataLoad}
showLink={false}
@ -162,13 +167,18 @@ export default function WebsiteDetails({ websiteId, shareId }) {
<EventsTable {...tableProps} onDataLoad={setEventsData} />
</div>
<div className="col-12 col-md-12 col-lg-8 pt-5 pb-5">
<EventsChart websiteId={websiteId} />
<EventsChart websiteId={websiteId} token={token} />
</div>
</div>
</>
)}
{view && (
<MenuLayout className={styles.view} menuClassName={styles.menu} menu={menuOptions}>
<MenuLayout
className={styles.view}
menuClassName={styles.menu}
contentClassName={styles.content}
menu={menuOptions}
>
<DetailsComponent {...tableProps} limit={false} />
</MenuLayout>
)}

View File

@ -10,6 +10,10 @@
font-size: var(--font-size-small);
}
.content {
min-height: 600px;
}
.backButton {
align-self: flex-start;
margin-bottom: 16px;

View File

@ -5,7 +5,7 @@ import { setDateRange } from 'redux/actions/websites';
import Button from './Button';
import Refresh from 'assets/redo.svg';
import Dots from 'assets/ellipsis-h.svg';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
import { getDateRange } from '../../lib/date';
export default function RefreshButton({ websiteId }) {

View File

@ -4,8 +4,8 @@ import useFetch from 'hooks/useFetch';
import styles from './ActiveUsers.module.css';
import { FormattedMessage } from 'react-intl';
export default function ActiveUsers({ websiteId, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, {}, { interval: 60000 });
export default function ActiveUsers({ websiteId, token, className }) {
const { data } = useFetch(`/api/website/${websiteId}/active`, { token }, { interval: 60000 });
const count = useMemo(() => {
return data?.[0]?.x || 0;
}, [data]);

View File

@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import { browserFilter } from 'lib/filters';
export default function BrowsersTable({ websiteId, limit, onExpand }) {
export default function BrowsersTable({ websiteId, token, limit, onExpand }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.browsers" defaultMessage="Browsers" />}
type="browser"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={browserFilter}
onExpand={onExpand}

View File

@ -3,13 +3,20 @@ import MetricsTable from './MetricsTable';
import { countryFilter, percentFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function CountriesTable({ websiteId, limit, onDataLoad = () => {}, onExpand }) {
export default function CountriesTable({
websiteId,
token,
limit,
onDataLoad = () => {},
onExpand,
}) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.countries" defaultMessage="Countries" />}
type="country"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={countryFilter}
onDataLoad={data => onDataLoad(percentFilter(data))}

View File

@ -4,13 +4,14 @@ import { deviceFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
import { getDeviceMessage } from 'components/messages';
export default function DevicesTable({ websiteId, limit, onExpand }) {
export default function DevicesTable({ websiteId, token, limit, onExpand }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.devices" defaultMessage="Devices" />}
type="device"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={deviceFilter}
renderLabel={({ x }) => getDeviceMessage(x)}

View File

@ -3,7 +3,7 @@ import tinycolor from 'tinycolor2';
import BarChart from './BarChart';
import { getTimezone, getDateArray, getDateLength } from 'lib/date';
import useFetch from 'hooks/useFetch';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
const COLORS = [
'#2680eb',
@ -16,7 +16,7 @@ const COLORS = [
'#85d044',
];
export default function EventsChart({ websiteId }) {
export default function EventsChart({ websiteId, token }) {
const dateRange = useDateRange(websiteId);
const { startDate, endDate, unit, modified } = dateRange;
const { data } = useFetch(
@ -26,6 +26,7 @@ export default function EventsChart({ websiteId }) {
end_at: +endDate,
unit,
tz: getTimezone(),
token,
},
{ update: [modified] },
);

View File

@ -3,13 +3,14 @@ import { FormattedMessage } from 'react-intl';
import MetricsTable from './MetricsTable';
import styles from './EventsTable.module.css';
export default function EventsTable({ websiteId, limit, onExpand, onDataLoad }) {
export default function EventsTable({ websiteId, token, limit, onExpand, onDataLoad }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.events" defaultMessage="Events" />}
type="event"
metric={<FormattedMessage id="metrics.actions" defaultMessage="Actions" />}
websiteId={websiteId}
token={token}
limit={limit}
renderLabel={({ x }) => <Label value={x} />}
onExpand={onExpand}

View File

@ -3,12 +3,12 @@ import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import Loading from 'components/common/Loading';
import useFetch from 'hooks/useFetch';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
import { formatShortTime, formatNumber, formatLongNumber } from 'lib/format';
import MetricCard from './MetricCard';
import styles from './MetricsBar.module.css';
export default function MetricsBar({ websiteId, className }) {
export default function MetricsBar({ websiteId, token, className }) {
const dateRange = useDateRange(websiteId);
const { startDate, endDate, modified } = dateRange;
const { data } = useFetch(
@ -16,6 +16,7 @@ export default function MetricsBar({ websiteId, className }) {
{
start_at: +startDate,
end_at: +endDate,
token,
},
{
update: [modified],

View File

@ -10,12 +10,13 @@ import useFetch from 'hooks/useFetch';
import Arrow from 'assets/arrow-right.svg';
import { percentFilter } from 'lib/filters';
import { formatNumber, formatLongNumber } from 'lib/format';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
import styles from './MetricsTable.module.css';
export default function MetricsTable({
websiteId,
websiteDomain,
token,
title,
metric,
type,
@ -37,6 +38,7 @@ export default function MetricsTable({
start_at: +startDate,
end_at: +endDate,
domain: websiteDomain,
token,
},
{ onDataLoad, delay: 300, update: [modified] },
);

View File

@ -3,13 +3,14 @@ import MetricsTable from './MetricsTable';
import { osFilter } from 'lib/filters';
import { FormattedMessage } from 'react-intl';
export default function OSTable({ websiteId, limit, onExpand }) {
export default function OSTable({ websiteId, token, limit, onExpand }) {
return (
<MetricsTable
title={<FormattedMessage id="metrics.operating-systems" defaultMessage="Operating system" />}
type="os"
metric={<FormattedMessage id="metrics.visitors" defaultMessage="Visitors" />}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={osFilter}
onExpand={onExpand}

View File

@ -5,7 +5,7 @@ import { urlFilter } from 'lib/filters';
import { FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
import MetricsTable from './MetricsTable';
export default function PagesTable({ websiteId, websiteDomain, limit, onExpand }) {
export default function PagesTable({ websiteId, token, websiteDomain, limit, onExpand }) {
const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [
@ -25,6 +25,7 @@ export default function PagesTable({ websiteId, websiteDomain, limit, onExpand }
limit ? null : <FilterButtons buttons={buttons} selected={filter} onClick={setFilter} />
}
websiteId={websiteId}
token={token}
limit={limit}
dataFilter={urlFilter}
filterOptions={{ domain: websiteDomain, raw: filter === FILTER_RAW }}

View File

@ -1,30 +0,0 @@
import React from 'react';
import ButtonGroup from 'components/common/ButtonGroup';
import { getDateRange } from 'lib/date';
import styles from './QuickButtons.module.css';
const options = [
{ label: '24h', value: '24hour' },
{ label: '7d', value: '7day' },
{ label: '30d', value: '30day' },
];
export default function QuickButtons({ value, onChange }) {
const selectedItem = options.find(item => item.value === value)?.value;
function handleClick(selected) {
if (selected !== value) {
onChange(getDateRange(selected));
}
}
return (
<ButtonGroup
size="xsmall"
className={styles.buttons}
items={options}
selectedItem={selectedItem}
onClick={handleClick}
/>
);
}

View File

@ -1,33 +0,0 @@
.buttons {
display: flex;
align-content: center;
position: absolute;
top: 0;
right: 0;
margin: auto;
}
.buttons button + button {
margin-left: 10px;
}
.buttons .button {
font-size: var(--font-size-xsmall);
padding: 4px 8px;
}
.active {
font-weight: 600;
}
@media only screen and (max-width: 768px) {
.buttons button:last-child {
display: none;
}
}
@media only screen and (max-width: 576px) {
.buttons {
display: none;
}
}

View File

@ -5,7 +5,13 @@ import { refFilter } from 'lib/filters';
import ButtonGroup from 'components/common/ButtonGroup';
import { FILTER_DOMAIN_ONLY, FILTER_COMBINED, FILTER_RAW } from 'lib/constants';
export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpand = () => {} }) {
export default function ReferrersTable({
websiteId,
websiteDomain,
token,
limit,
onExpand = () => {},
}) {
const [filter, setFilter] = useState(FILTER_COMBINED);
const buttons = [
@ -40,6 +46,7 @@ export default function ReferrersTable({ websiteId, websiteDomain, limit, onExpa
}
websiteId={websiteId}
websiteDomain={websiteDomain}
token={token}
limit={limit}
dataFilter={refFilter}
filterOptions={{

View File

@ -3,17 +3,18 @@ import { useDispatch } from 'react-redux';
import classNames from 'classnames';
import PageviewsChart from './PageviewsChart';
import MetricsBar from './MetricsBar';
import WebsiteHeader from './WebsiteHeader';
import DateFilter from 'components/common/DateFilter';
import StickyHeader from 'components/helpers/StickyHeader';
import useFetch from 'hooks/useFetch';
import useDateRange from 'hooks/useDateRange';
import { getDateArray, getDateLength, getTimezone } from 'lib/date';
import { setDateRange } from 'redux/actions/websites';
import styles from './WebsiteChart.module.css';
import WebsiteHeader from './WebsiteHeader';
import { useDateRange } from '../../hooks/useDateRange';
export default function WebsiteChart({
websiteId,
token,
title,
stickyHeader = false,
showLink = false,
@ -30,6 +31,7 @@ export default function WebsiteChart({
end_at: +endDate,
unit,
tz: getTimezone(),
token,
},
{ onDataLoad, update: [modified] },
);
@ -50,7 +52,7 @@ export default function WebsiteChart({
return (
<>
<WebsiteHeader websiteId={websiteId} title={title} showLink={showLink} />
<WebsiteHeader websiteId={websiteId} token={token} title={title} showLink={showLink} />
<div className={classNames(styles.header, 'row')}>
<StickyHeader
className={classNames(styles.metrics, 'col row')}
@ -58,7 +60,7 @@ export default function WebsiteChart({
enabled={stickyHeader}
>
<div className="col-12 col-lg-9">
<MetricsBar websiteId={websiteId} />
<MetricsBar websiteId={websiteId} token={token} />
</div>
<div className={classNames(styles.filter, 'col-12 col-lg-3')}>
<DateFilter

View File

@ -2,18 +2,18 @@ import React from 'react';
import { FormattedMessage } from 'react-intl';
import Link from 'components/common/Link';
import PageHeader from 'components/layout/PageHeader';
import RefreshButton from 'components/common/RefreshButton';
import ButtonLayout from 'components/layout/ButtonLayout';
import Icon from 'components/common/Icon';
import ActiveUsers from './ActiveUsers';
import Arrow from 'assets/arrow-right.svg';
import styles from './WebsiteHeader.module.css';
import RefreshButton from '../common/RefreshButton';
import ButtonLayout from '../layout/ButtonLayout';
import Icon from '../common/Icon';
export default function WebsiteHeader({ websiteId, title, showLink = false }) {
export default function WebsiteHeader({ websiteId, token, title, showLink = false }) {
return (
<PageHeader>
<div className={styles.title}>{title}</div>
<ActiveUsers className={styles.active} websiteId={websiteId} />
<ActiveUsers className={styles.active} websiteId={websiteId} token={token} />
<ButtonLayout>
<RefreshButton websiteId={websiteId} />
{showLink && (

View File

@ -10,7 +10,7 @@ import DateFilter from 'components/common/DateFilter';
import Dots from 'assets/ellipsis-h.svg';
import { getTimezone } from 'lib/date';
import { setItem } from 'lib/web';
import { useDateRange } from 'hooks/useDateRange';
import useDateRange from 'hooks/useDateRange';
import { setDateRange } from 'redux/actions/websites';
import styles from './ProfileSettings.module.css';

View File

@ -3,7 +3,7 @@ import { parseISO } from 'date-fns';
import { getDateRange } from 'lib/date';
import { getItem } from 'lib/web';
export function useDateRange(websiteId, defaultDateRange = '7day') {
export default function useDateRange(websiteId, defaultDateRange = '24hour') {
const globalDefault = getItem('umami.date-range');
if (globalDefault) {

View File

@ -1,9 +1,49 @@
import { parse } from 'cookie';
import { parseSecureToken } from './crypto';
import { parseSecureToken, parseToken } from './crypto';
import { AUTH_COOKIE_NAME } from './constants';
import { getWebsiteById } from './queries';
export async function verifyAuthToken(req) {
export async function getAuthToken(req) {
const token = parse(req.headers.cookie || '')[AUTH_COOKIE_NAME];
return parseSecureToken(token);
}
export async function isValidToken(token, validation) {
try {
const result = await parseToken(token);
if (typeof validation === 'object') {
return !Object.keys(validation).find(key => result[key] !== validation[key]);
} else if (typeof validation === 'function') {
return validation(result);
}
} catch (e) {
return false;
}
return false;
}
export async function allowQuery(req, skipToken) {
const { id, token } = req.query;
const websiteId = +id;
const website = await getWebsiteById(websiteId);
if (website) {
if (token && !skipToken) {
return isValidToken(token, { website_id: websiteId });
}
const authToken = await getAuthToken(req);
if (authToken) {
const { user_id, is_admin } = authToken;
return is_admin || website.user_id === user_id;
}
}
return false;
}

View File

@ -27,7 +27,7 @@ export function uuid(...args) {
return v5(args.join(''), salt());
}
export function isValidId(s) {
export function isValidUuid(s) {
return validate(s);
}

View File

@ -1,6 +1,6 @@
import cors from 'cors';
import { verifySession } from './session';
import { verifyAuthToken } from './auth';
import { getSession } from './session';
import { getAuthToken } from './auth';
import { unauthorized, badRequest, serverError } from './response';
export function use(middleware) {
@ -21,7 +21,7 @@ export const useSession = use(async (req, res, next) => {
let session;
try {
session = await verifySession(req);
session = await getSession(req);
} catch (e) {
return serverError(res, e.message);
}
@ -35,13 +35,7 @@ export const useSession = use(async (req, res, next) => {
});
export const useAuth = use(async (req, res, next) => {
let token;
try {
token = await verifyAuthToken(req);
} catch (e) {
return serverError(res, e.message);
}
const token = await getAuthToken(req);
if (!token) {
return unauthorized(res);

View File

@ -1,8 +1,8 @@
import { getWebsiteByUuid, getSessionByUuid, createSession } from 'lib/queries';
import { getClientInfo } from 'lib/request';
import { uuid, isValidId } from 'lib/crypto';
import { uuid, isValidUuid } from 'lib/crypto';
export async function verifySession(req) {
export async function getSession(req) {
const { payload } = req.body;
if (!payload) {
@ -11,7 +11,7 @@ export async function verifySession(req) {
const { website: website_uuid, hostname, screen, language } = payload;
if (!isValidId(website_uuid)) {
if (!isValidUuid(website_uuid)) {
throw new Error(`Invalid website: ${website_uuid}`);
}

View File

@ -1,5 +1,6 @@
import { getWebsiteByShareId } from 'lib/queries';
import { ok, notFound, methodNotAllowed } from 'lib/response';
import { createToken } from 'lib/crypto';
export default async (req, res) => {
const { id } = req.query;
@ -8,7 +9,10 @@ export default async (req, res) => {
const website = await getWebsiteByShareId(id);
if (website) {
return ok(res, website);
const websiteId = website.website_id;
const token = await createToken({ website_id: websiteId });
return ok(res, { websiteId, token });
}
return notFound(res);

View File

@ -1,12 +1,18 @@
import { getActiveVisitors } from 'lib/queries';
import { methodNotAllowed, ok } from 'lib/response';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
if (req.method === 'GET') {
const { id } = req.query;
const website_id = +id;
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const result = await getActiveVisitors(website_id);
const { id } = req.query;
const websiteId = +id;
const result = await getActiveVisitors(websiteId);
return ok(res, result);
}

View File

@ -1,11 +1,16 @@
import moment from 'moment-timezone';
import { getEvents } from 'lib/queries';
import { ok, badRequest, methodNotAllowed } from 'lib/response';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
const unitTypes = ['year', 'month', 'hour', 'day'];
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, start_at, end_at, unit, tz } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {

View File

@ -1,30 +1,30 @@
import { deleteWebsite, getWebsiteById } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
await useAuth(req, res);
const { id } = req.query;
const { user_id, is_admin } = req.auth;
const { id, share_id } = req.query;
const websiteId = +id;
const website = await getWebsiteById(websiteId);
if (req.method === 'GET') {
if (is_admin || website.user_id === user_id || (share_id && website.share_id === share_id)) {
return ok(res, website);
if (!(await allowQuery(req))) {
return unauthorized(res);
}
return unauthorized(res);
const website = await getWebsiteById(websiteId);
return ok(res, website);
}
if (req.method === 'DELETE') {
if (is_admin || website.user_id === user_id) {
await deleteWebsite(websiteId);
return ok(res);
if (!(await allowQuery(req, true))) {
return unauthorized(res);
}
return unauthorized(res);
await deleteWebsite(websiteId);
return ok(res);
}
return methodNotAllowed(res);

View File

@ -1,9 +1,15 @@
import { getMetrics } from 'lib/queries';
import { methodNotAllowed, ok } from 'lib/response';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, start_at, end_at } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);

View File

@ -1,21 +1,26 @@
import moment from 'moment-timezone';
import { getPageviews } from 'lib/queries';
import { ok, badRequest, methodNotAllowed } from 'lib/response';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
const unitTypes = ['year', 'month', 'hour', 'day'];
export default async (req, res) => {
if (req.method === 'GET') {
const { id, start_at, end_at, unit, tz } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res);
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, start_at, end_at, unit, tz } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res);
}
const [pageviews, uniques] = await Promise.all([
getPageviews(websiteId, startDate, endDate, tz, unit, '*'),
getPageviews(websiteId, startDate, endDate, tz, unit, 'distinct session_id'),

View File

@ -1,6 +1,7 @@
import { getRankings } from 'lib/queries';
import { ok, badRequest, methodNotAllowed } from 'lib/response';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { DOMAIN_REGEX } from 'lib/constants';
import { allowQuery } from 'lib/auth';
const sessionColumns = ['browser', 'os', 'device', 'country'];
const pageviewColumns = ['url', 'referrer'];
@ -26,7 +27,12 @@ function getColumn(type) {
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, type, start_at, end_at, domain } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);

View File

@ -15,21 +15,21 @@ export default async (req, res) => {
if (website_id) {
const website = await getWebsiteById(website_id);
if (website.user_id === user_id || is_admin) {
let { share_id } = website;
if (enable_share_url) {
share_id = share_id ? share_id : getRandomChars(8);
} else {
share_id = null;
}
await updateWebsite(website_id, { name, domain, share_id });
return ok(res);
if (website.user_id !== user_id && !is_admin) {
return unauthorized(res);
}
return unauthorized(res);
let { share_id } = website;
if (enable_share_url) {
share_id = share_id ? share_id : getRandomChars(8);
} else {
share_id = null;
}
await updateWebsite(website_id, { name, domain, share_id });
return ok(res);
} else {
const website_uuid = uuid();
const share_id = enable_share_url ? getRandomChars(8) : null;

View File

@ -7,13 +7,14 @@ export default async (req, res) => {
const { user_id: current_user_id, is_admin } = req.auth;
const { user_id } = req.query;
const userId = +user_id;
if (req.method === 'GET') {
if (user_id && !is_admin) {
if (userId !== current_user_id && !is_admin) {
return unauthorized(res);
}
const websites = await getUserWebsites(+user_id || current_user_id);
const websites = await getUserWebsites(userId || current_user_id);
return ok(res, websites);
}

View File

@ -14,9 +14,11 @@ export default function SharePage() {
return null;
}
const { websiteId, token } = data;
return (
<Layout>
<WebsiteDetails websiteId={data.website_id} shareId={shareId} />
<WebsiteDetails websiteId={websiteId} token={token} />
</Layout>
);
}