Use token authentication for API requests.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,9 +1,49 @@
import { parse } from 'cookie'; import { parse } from 'cookie';
import { parseSecureToken } from './crypto'; import { parseSecureToken, parseToken } from './crypto';
import { AUTH_COOKIE_NAME } from './constants'; 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]; const token = parse(req.headers.cookie || '')[AUTH_COOKIE_NAME];
return parseSecureToken(token); 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()); return v5(args.join(''), salt());
} }
export function isValidId(s) { export function isValidUuid(s) {
return validate(s); return validate(s);
} }

View File

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

View File

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

View File

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

View File

@ -1,12 +1,18 @@
import { getActiveVisitors } from 'lib/queries'; 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) => { export default async (req, res) => {
if (req.method === 'GET') { if (req.method === 'GET') {
const { id } = req.query; if (!(await allowQuery(req))) {
const website_id = +id; 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); return ok(res, result);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,21 +15,21 @@ export default async (req, res) => {
if (website_id) { if (website_id) {
const website = await getWebsiteById(website_id); const website = await getWebsiteById(website_id);
if (website.user_id === user_id || is_admin) { if (website.user_id !== user_id && !is_admin) {
let { share_id } = website; return unauthorized(res);
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);
} }
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 { } else {
const website_uuid = uuid(); const website_uuid = uuid();
const share_id = enable_share_url ? getRandomChars(8) : null; 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: current_user_id, is_admin } = req.auth;
const { user_id } = req.query; const { user_id } = req.query;
const userId = +user_id;
if (req.method === 'GET') { if (req.method === 'GET') {
if (user_id && !is_admin) { if (userId !== current_user_id && !is_admin) {
return unauthorized(res); return unauthorized(res);
} }
const websites = await getUserWebsites(+user_id || current_user_id); const websites = await getUserWebsites(userId || current_user_id);
return ok(res, websites); return ok(res, websites);
} }

View File

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