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 00000000..b9ff0aaa Binary files /dev/null and b/public/favicon.ico differ 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"