diff --git a/components/Chart.js b/components/Chart.js index 1324e51b29..91e7bb39a1 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 0000000000..586798acae --- /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 0000000000..659e1b9c10 --- /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 bfc6de0e6f..a02b906d56 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 3c6446293a..1c9d0211ab 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 3ad591e6ca..bbd0dd7657 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 502ab1f7d4..32ac09241a 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 9ad8bc9579..5e977047ae 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 0000000000..0bd2478d3c --- /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 a4bfb18a1f..e9570a7bb3 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 0000000000..ed7b731570 --- /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 c518752016..0bca544e9e 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 }) {