Auth and session middleware.

This commit is contained in:
Mike Cao 2020-07-27 23:52:14 -07:00
parent 590a70c2ff
commit d81ee3932d
14 changed files with 142 additions and 73 deletions

36
components/Chart.js vendored
View File

@ -1,7 +1,7 @@
import React, { useState, useMemo, useRef, useEffect } from 'react';
import ChartJS from 'chart.js';
import { format, subDays, subHours, startOfHour } from 'date-fns';
import { get } from 'lib/web';
import { getTimezone, getLocalTime } from 'lib/date';
export default function Chart({ websiteId, startDate, endDate }) {
const [data, setData] = useState();
@ -9,21 +9,7 @@ export default function Chart({ websiteId, startDate, endDate }) {
const chart = useRef();
const metrics = useMemo(() => {
if (data) {
const points = {};
const now = startOfHour(new Date());
for (let i = 0; i <= 168; i++) {
const d = new Date(subHours(now, 168 - i));
const key = format(d, 'yyyy-MM-dd-HH');
points[key] = { t: startOfHour(d).toISOString(), y: 0 };
}
data.pageviews.forEach(e => {
const key = format(new Date(e.created_at), 'yyyy-MM-dd-HH');
points[key].y += 1;
});
return points;
return data.pageviews.map(({ t, y }) => ({ t: getLocalTime(t), y }));
}
}, [data]);
console.log(metrics);
@ -31,8 +17,9 @@ export default function Chart({ websiteId, startDate, endDate }) {
async function loadData() {
setData(
await get(`/api/website/${websiteId}/pageviews`, {
start_at: startDate,
end_at: endDate,
start_at: +startDate,
end_at: +endDate,
tz: getTimezone(),
}),
);
}
@ -47,6 +34,9 @@ export default function Chart({ websiteId, startDate, endDate }) {
label: 'page views',
data: Object.values(metrics),
lineTension: 0,
backgroundColor: 'rgb(38, 128, 235, 0.1)',
borderColor: 'rgb(13, 102, 208, 0.2)',
borderWidth: 1,
},
],
},
@ -65,19 +55,13 @@ export default function Chart({ websiteId, startDate, endDate }) {
{
type: 'time',
distribution: 'series',
offset: true,
time: {
unit: 'hour',
displayFormats: {
hour: 'ddd M/DD',
day: 'ddd M/DD',
},
tooltipFormat: 'ddd M/DD hA',
},
ticks: {
autoSkip: true,
minRotation: 0,
maxRotation: 0,
maxTicksLimit: 7,
},
},
],
yAxes: [

8
lib/auth.js Normal file
View File

@ -0,0 +1,8 @@
import { parse } from 'cookie';
import { verifySecureToken } from './crypto';
export default async req => {
const token = parse(req.headers.cookie)['umami.auth'];
return verifySecureToken(token);
};

11
lib/date.js Normal file
View File

@ -0,0 +1,11 @@
import moment from 'moment-timezone';
import { addMinutes } from 'date-fns';
export function getTimezone() {
const tz = moment.tz.guess();
return moment.tz.zone(tz).abbr(new Date().getTimezoneOffset());
}
export function getLocalTime(t) {
return addMinutes(new Date(t), new Date().getTimezoneOffset());
}

View File

@ -32,6 +32,16 @@ export async function getWebsite(website_uuid) {
);
}
export async function getWebsites(user_id) {
return runQuery(
prisma.website.findMany({
where: {
user_id,
},
}),
);
}
export async function createSession(website_id, data) {
return runQuery(
prisma.session.create({
@ -126,3 +136,29 @@ export async function getPageviews(website_id, start_at, end_at) {
}),
);
}
export async function getPageviewData(
website_id,
start_at,
end_at,
timezone = 'utc',
unit = 'day',
count = '*',
) {
return runQuery(
prisma.queryRaw(
`
select date_trunc('${unit}', created_at at time zone '${timezone}') t,
count(${count}) y
from pageview
where website_id=$1
and created_at between $2 and $3
group by 1
order by 1
`,
website_id,
start_at,
end_at,
),
);
}

View File

@ -1,4 +1,6 @@
import cors from 'cors';
import session from './session';
import auth from './auth';
export function use(middleware) {
return (req, res) =>
@ -13,3 +15,21 @@ export function use(middleware) {
}
export const useCors = use(cors());
export const useSession = use(async (req, res, next) => {
try {
req.session = await session(req);
} catch {
return res.status(400).end();
}
next();
});
export const useAuth = use(async (req, res, next) => {
try {
req.auth = await auth(req);
} catch {
return res.status(401).end();
}
next();
});

View File

@ -1,5 +1,5 @@
import { getWebsite, getSession, createSession } from 'lib/db';
import { getCountry, getDevice, getIpAddress } from 'lib/utils';
import { getCountry, getDevice, getIpAddress } from 'lib/request';
import { uuid, isValidId, verifyToken } from 'lib/crypto';
export default async req => {
@ -46,6 +46,8 @@ export default async req => {
session_id,
session_uuid,
};
} else {
throw new Error(`Invalid website: ${website_uuid}`);
}
}
}

View File

@ -53,8 +53,8 @@
"is-localhost-ip": "^1.4.0",
"jose": "^1.27.2",
"maxmind": "^4.1.3",
"moment-timezone": "^0.5.31",
"next": "9.3.5",
"next-cookies": "^2.0.3",
"node-fetch": "^2.6.0",
"promise-polyfill": "^8.1.3",
"react": "16.13.1",

View File

@ -1,13 +1,12 @@
import { savePageView, saveEvent } from 'lib/db';
import { useCors } from 'lib/middleware';
import checkSession from 'lib/session';
import { useCors, useSession } from 'lib/middleware';
import { createToken } from 'lib/crypto';
export default async (req, res) => {
await useCors(req, res);
await useSession(req, res);
const session = await checkSession(req);
const { session } = req;
const token = await createToken(session);
const { website_id, session_id } = session;
const { type, payload } = req.body;

12
pages/api/website.js Normal file
View File

@ -0,0 +1,12 @@
import { getWebsites } from 'lib/db';
import { useAuth } from 'lib/middleware';
export default async (req, res) => {
await useAuth(req, res);
const { user_id } = req.auth;
const websites = await getWebsites(user_id);
res.status(200).json({ websites });
};

View File

@ -1,10 +1,15 @@
import { getPageviews } from 'lib/db';
import { getPageviewData } from 'lib/db';
import { useAuth } from 'lib/middleware';
export default async (req, res) => {
console.log(req.query);
const { id, start_at, end_at } = req.query;
await useAuth(req, res);
const pageviews = await getPageviews(+id, new Date(+start_at), new Date(+end_at));
const { id, start_at, end_at, tz } = req.query;
res.status(200).json({ pageviews });
const [pageviews, uniques] = await Promise.all([
getPageviewData(+id, new Date(+start_at), new Date(+end_at), tz, 'day', '*'),
getPageviewData(+id, new Date(+start_at), new Date(+end_at), tz, 'day', 'distinct session_id'),
]);
res.status(200).json({ pageviews, uniques });
};

View File

@ -0,0 +1,13 @@
import { getPageviews } from 'lib/db';
import { useAuth } from 'lib/middleware';
export default async (req, res) => {
await useAuth(req, res);
console.log(req.query);
const { id, start_at, end_at } = req.query;
const pageviews = await getPageviews(+id, new Date(+start_at), new Date(+end_at));
res.status(200).json({ pageviews });
};

View File

@ -1,9 +1,10 @@
import React from 'react';
import Link from 'next/link';
import cookies from 'next-cookies';
import { parse } from 'cookie';
import Layout from 'components/Layout';
import Chart from 'components/Chart';
import { verifySecureToken } from 'lib/crypto';
import { subDays, endOfDay } from 'date-fns';
export default function HomePage({ username }) {
return (
@ -14,8 +15,8 @@ export default function HomePage({ username }) {
<div>
<Chart
websiteId={3}
startDate={Date.now() - 1000 * 60 * 60 * 24 * 7}
endDate={Date.now()}
startDate={subDays(endOfDay(new Date()), 6)}
endDate={endOfDay(new Date())}
/>
</div>
<Link href="/logout">
@ -25,8 +26,8 @@ export default function HomePage({ username }) {
);
}
export async function getServerSideProps(context) {
const token = cookies(context)['umami.auth'];
export async function getServerSideProps({ req, res }) {
const token = parse(req.headers.cookie)['umami.auth'];
try {
const payload = await verifySecureToken(token);
@ -37,8 +38,6 @@ export async function getServerSideProps(context) {
},
};
} catch {
const { res } = context;
res.statusCode = 303;
res.setHeader('Location', '/login');
res.end();

View File

@ -1374,11 +1374,6 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/cookie@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -1414,11 +1409,6 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/object-assign@^4.0.30":
version "4.0.30"
resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652"
integrity sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI=
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -2662,7 +2652,7 @@ convert-source-map@^0.3.3:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
cookie@^0.4.0, cookie@^0.4.1:
cookie@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
@ -5511,7 +5501,14 @@ mkdirp@^1.0.3:
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
moment@^2.10.2:
moment-timezone@^0.5.31:
version "0.5.31"
resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05"
integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA==
dependencies:
moment ">= 2.9.0"
"moment@>= 2.9.0", moment@^2.10.2:
version "2.27.0"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.27.0.tgz#8bff4e3e26a236220dfe3e36de756b6ebaa0105d"
integrity sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ==
@ -5596,13 +5593,6 @@ neo-async@^2.5.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-cookies@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/next-cookies/-/next-cookies-2.0.3.tgz#5a3eabcb6afa9b4d4ade69dfaaad749d16cd4a9a"
integrity sha512-YVCQzwZx+sz+KqLO4y9niHH9jjz6jajlEQbAKfsYVT6DOfngb/0k5l6vFK4rmpExVug96pGag8OBsdSRL9FZhQ==
dependencies:
universal-cookie "^4.0.2"
next-tick@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
@ -8719,16 +8709,6 @@ unist-util-visit@^2.0.0:
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
universal-cookie@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.3.tgz#c2fa59127260e6ad21ef3e0cdd66ad453cbc41f6"
integrity sha512-YbEHRs7bYOBTIWedTR9koVEe2mXrq+xdjTJZcoKJK/pQaE6ni28ak2AKXFpevb+X6w3iU5SXzWDiJkmpDRb9qw==
dependencies:
"@types/cookie" "^0.3.3"
"@types/object-assign" "^4.0.30"
cookie "^0.4.0"
object-assign "^4.1.1"
unquote@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"