From d81ee3932d4447548099bb0ae3dff0bf46810c65 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Mon, 27 Jul 2020 23:52:14 -0700 Subject: [PATCH] Auth and session middleware. --- components/Chart.js | 36 ++++++++------------------- lib/auth.js | 8 ++++++ lib/date.js | 11 +++++++++ lib/db.js | 36 +++++++++++++++++++++++++++ lib/middleware.js | 20 +++++++++++++++ lib/{utils.js => request.js} | 0 lib/session.js | 4 ++- package.json | 2 +- pages/api/collect.js | 7 +++--- pages/api/website.js | 12 +++++++++ pages/api/website/[id]/pageviews.js | 15 ++++++++---- pages/api/website/[id]/summary.js | 13 ++++++++++ pages/index.js | 13 +++++----- yarn.lock | 38 +++++++---------------------- 14 files changed, 142 insertions(+), 73 deletions(-) create mode 100644 lib/auth.js create mode 100644 lib/date.js rename lib/{utils.js => request.js} (100%) create mode 100644 pages/api/website.js create mode 100644 pages/api/website/[id]/summary.js diff --git a/components/Chart.js b/components/Chart.js index 1324e51b..91e7bb39 100644 --- a/components/Chart.js +++ b/components/Chart.js @@ -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: [ diff --git a/lib/auth.js b/lib/auth.js new file mode 100644 index 00000000..586798ac --- /dev/null +++ b/lib/auth.js @@ -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); +}; diff --git a/lib/date.js b/lib/date.js new file mode 100644 index 00000000..659e1b9c --- /dev/null +++ b/lib/date.js @@ -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()); +} diff --git a/lib/db.js b/lib/db.js index bfc6de0e..a02b906d 100644 --- a/lib/db.js +++ b/lib/db.js @@ -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, + ), + ); +} diff --git a/lib/middleware.js b/lib/middleware.js index 3c644629..1c9d0211 100644 --- a/lib/middleware.js +++ b/lib/middleware.js @@ -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(); +}); diff --git a/lib/utils.js b/lib/request.js similarity index 100% rename from lib/utils.js rename to lib/request.js diff --git a/lib/session.js b/lib/session.js index 3ad591e6..bbd0dd76 100644 --- a/lib/session.js +++ b/lib/session.js @@ -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}`); } } } diff --git a/package.json b/package.json index 502ab1f7..32ac0924 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pages/api/collect.js b/pages/api/collect.js index 9ad8bc95..5e977047 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -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; diff --git a/pages/api/website.js b/pages/api/website.js new file mode 100644 index 00000000..0bd2478d --- /dev/null +++ b/pages/api/website.js @@ -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 }); +}; diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js index a4bfb18a..e9570a7b 100644 --- a/pages/api/website/[id]/pageviews.js +++ b/pages/api/website/[id]/pageviews.js @@ -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 }); }; diff --git a/pages/api/website/[id]/summary.js b/pages/api/website/[id]/summary.js new file mode 100644 index 00000000..ed7b7315 --- /dev/null +++ b/pages/api/website/[id]/summary.js @@ -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 }); +}; diff --git a/pages/index.js b/pages/index.js index c5187520..0bca544e 100644 --- a/pages/index.js +++ b/pages/index.js @@ -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 }) {
@@ -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(); diff --git a/yarn.lock b/yarn.lock index c3072b95..7082c1de 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"