From 58a1c634077be25ec06199354be37e2659e12e92 Mon Sep 17 00:00:00 2001 From: Mike Cao Date: Sat, 18 Jul 2020 10:36:46 -0700 Subject: [PATCH] Added CORS middleware. Updated umami script. --- lib/middleware.js | 20 ++++++++++ lib/utils.js | 75 +++++++++++++++++++++----------------- package.json | 1 + pages/api/collect.js | 13 ++++--- pages/api/session.js | 43 ++++++++++++++-------- prisma/schema.prisma | 1 + public/favicon.ico | Bin 0 -> 3335 bytes scripts/umami/index.js | 51 +++++++++++++++----------- sql/schema.postgresql.sql | 3 +- yarn.lock | 15 +++++++- 10 files changed, 144 insertions(+), 78 deletions(-) create mode 100644 lib/middleware.js create mode 100644 public/favicon.ico diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 00000000..e2144e74 --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,20 @@ +import cors from 'cors'; + +export function use(middleware) { + return (req, res) => + new Promise((resolve, reject) => { + middleware(req, res, result => { + if (result instanceof Error) { + return reject(result); + } + return resolve(result); + }); + }); +} + +export const allowPost = use( + cors({ + origin: '*', + methods: ['POST', 'OPTIONS'], + }), +); diff --git a/lib/utils.js b/lib/utils.js index 18b94fa1..60397e94 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -61,48 +61,55 @@ export async function getCountry(req, ip) { } export async function parseSessionRequest(req) { - const ip = getIpAddress(req); - const { website_id, screen, language } = req.body; - const { userAgent, browser, os } = getDevice(req); - const country = await getCountry(req, ip); - const session_id = hash(`${website_id}${ip}${userAgent}${os}`); + if (req.method === 'POST') { + const ip = getIpAddress(req); + const { website_id, hostname, screen, language } = req.body; + const { userAgent, browser, os } = getDevice(req); + const country = await getCountry(req, ip); + const session_id = hash(`${website_id}${hostname}${ip}${userAgent}${os}`); - return { - website_id, - session_id, - browser, - os, - screen, - language, - country, - }; + return { + website_id, + session_id, + hostname, + browser, + os, + screen, + language, + country, + }; + } + + return {}; } export function parseCollectRequest(req) { - const { type, payload } = req.body; + if (req.method === 'POST') { + const { type, payload } = req.body; - if (payload.session) { - const { - url, - referrer, - session: { website_id, session_id, time, hash: validationHash }, - } = payload; - - if ( - validHash(website_id) && - validHash(session_id) && - validHash(validationHash) && - hash(`${website_id}${session_id}${time}`) === validationHash - ) { - return { - valid: true, - type, - session_id, + if (payload.session) { + const { url, referrer, - }; + session: { website_id, session_id, time, hash: validationHash }, + } = payload; + + if ( + validHash(website_id) && + validHash(session_id) && + validHash(validationHash) && + hash(`${website_id}${session_id}${time}`) === validationHash + ) { + return { + success: 1, + type, + session_id, + url, + referrer, + }; + } } } - return { valid: false }; + return { success: 0 }; } diff --git a/package.json b/package.json index 1f768b5b..2b105439 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "@prisma/client": "2.2.2", "chart.js": "^2.9.3", "classnames": "^2.2.6", + "cors": "^2.8.5", "date-fns": "^2.14.0", "detect-browser": "^5.1.1", "dotenv": "^8.2.0", diff --git a/pages/api/collect.js b/pages/api/collect.js index 1c9334bf..d1c827c5 100644 --- a/pages/api/collect.js +++ b/pages/api/collect.js @@ -1,18 +1,21 @@ import { parseCollectRequest } from 'lib/utils'; import { savePageView } from 'lib/db'; +import { allowPost } from 'lib/middleware'; export default async (req, res) => { + await allowPost(req, res); + const values = parseCollectRequest(req); - if (values.valid) { + if (values.success) { const { type, session_id, url, referrer } = values; if (type === 'pageview') { - await savePageView(session_id, url, referrer); + await savePageView(session_id, url, referrer).catch(() => { + values.success = 0; + }); } } - res.setHeader('Access-Control-Allow-Origin', '*'); - - res.status(200).json({ status: values.valid }); + res.status(200).json({ success: values.success }); }; diff --git a/pages/api/session.js b/pages/api/session.js index 9e1cf2f1..bca7ce7a 100644 --- a/pages/api/session.js +++ b/pages/api/session.js @@ -1,11 +1,16 @@ import { getWebsite, getSession, createSession } from 'lib/db'; import { hash, parseSessionRequest } from 'lib/utils'; +import { allowPost } from 'lib/middleware'; export default async (req, res) => { - let result = { time: Date.now() }; + await allowPost(req, res); + + let result = { success: 0, time: Date.now() }; + const { website_id, session_id, + hostname, browser, os, screen, @@ -13,24 +18,32 @@ export default async (req, res) => { country, } = await parseSessionRequest(req); - const website = await getWebsite(website_id); + if (website_id && session_id) { + const website = await getWebsite(website_id); - if (website) { - const session = await getSession(session_id); + if (website) { + const session = await getSession(session_id); - if (!session) { - await createSession(website_id, session_id, { browser, os, screen, language, country }); + if (!session) { + await createSession(website_id, session_id, { + hostname, + browser, + os, + screen, + language, + country, + }); + } + + result = { + ...result, + success: 1, + session_id, + website_id, + hash: hash(`${website_id}${session_id}${result.time}`), + }; } - - result = { - ...result, - session_id, - website_id, - hash: hash(`${website_id}${session_id}${result.time}`), - }; } - res.setHeader('Access-Control-Allow-Origin', '*'); - res.status(200).json(result); }; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e62cd79c..372681fd 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -34,6 +34,7 @@ model session { browser String? country String? created_at DateTime? @default(now()) + hostname String? language String? os String? screen String? diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b9ff0aaa1a926ae21e560ab87f89086c1d4fdde0 GIT binary patch literal 3335 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 R-4iJTAdRiAyfSo&`VX&0A&>w7 literal 0 HcmV?d00001 diff --git a/scripts/umami/index.js b/scripts/umami/index.js index f86896da..1c474f98 100644 --- a/scripts/umami/index.js +++ b/scripts/umami/index.js @@ -11,16 +11,37 @@ const { document, } = window; -function post(url, params) { - return fetch(url, { +const post = (url, params) => + fetch(url, { method: 'post', cache: 'no-cache', headers: { + Accept: 'application/json', 'Content-Type': 'application/json', }, - body: params, + body: JSON.stringify(params), }).then(res => res.json()); -} + +const createSession = data => + post(`${HOST_URL}/api/session`, data).then(({ success, ...session }) => { + if (success) { + store.setItem(SESSION_VAR, JSON.stringify(session)); + return success; + } + }); + +const getSession = () => JSON.parse(store.getItem(SESSION_VAR)); + +const pageView = (url, referrer) => + post(`${HOST_URL}/api/collect`, { + type: 'pageview', + payload: { url, referrer, session: getSession() }, + }).then(({ success }) => { + if (!success) { + store.removeItem(SESSION_VAR); + } + return success; + }); const script = document.querySelector('script[data-website-id]'); @@ -31,26 +52,12 @@ if (script) { const referrer = document.referrer; const screen = `${width}x${height}`; const url = `${pathname}${search}`; + const data = { website_id, hostname, url, screen, language }; if (!store.getItem(SESSION_VAR)) { - post(`${HOST_URL}/api/session`, { - website_id, - hostname, - url, - screen, - language, - }).then(session => { - store.setItem(SESSION_VAR, JSON.stringify(session)); - }); + createSession(data).then(success => success && pageView(url, referrer)); + } else { + pageView(url, referrer).then(success => !success && createSession(data)); } - - post(`${HOST_URL}/api/collect`, { - type: 'pageview', - payload: { url, referrer, session: JSON.parse(store.getItem(SESSION_VAR)) }, - }).then(response => { - if (!response.status) { - store.removeItem(SESSION_VAR); - } - }); } } diff --git a/sql/schema.postgresql.sql b/sql/schema.postgresql.sql index 09c82bbb..b5ddcbac 100644 --- a/sql/schema.postgresql.sql +++ b/sql/schema.postgresql.sql @@ -1,6 +1,6 @@ create table website ( website_id uuid primary key, - hostname varchar(255) unique not null, + hostname varchar(100) unique not null, created_at timestamp with time zone default current_timestamp ); @@ -8,6 +8,7 @@ create table session ( session_id uuid primary key, website_id uuid references website(website_id) on delete cascade, created_at timestamp with time zone default current_timestamp, + hostname varchar(100), browser varchar(20), os varchar(20), screen varchar(11), diff --git a/yarn.lock b/yarn.lock index 57f2bac5..00fbabae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2619,6 +2619,14 @@ core-util-is@~1.0.0: resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" integrity sha1-tf1UIgqivFq1eqtxQMlAdUUDwac= +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" @@ -5513,7 +5521,7 @@ num2fraction@^1.2.2: resolved "https://registry.yarnpkg.com/num2fraction/-/num2fraction-1.2.2.tgz#6f682b6a027a4e9ddfa4564cd2589d1d4e669ede" integrity sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4= -object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: +object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" integrity sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= @@ -8386,6 +8394,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +vary@^1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" + integrity sha1-IpnwLG3tMNSllhsLn3RSShj2NPw= + vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"