Initial commit

Created from https://vercel.com/new
This commit is contained in:
WaylonWalker 2021-12-08 18:25:42 +00:00
commit 4047a7ec23
378 changed files with 29334 additions and 0 deletions

29
pages/api/account/[id].js Normal file
View file

@ -0,0 +1,29 @@
import { getAccountById, deleteAccount } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
const { is_admin } = req.auth;
const { id } = req.query;
const user_id = +id;
if (!is_admin) {
return unauthorized(res);
}
if (req.method === 'GET') {
const account = await getAccountById(user_id);
return ok(res, account);
}
if (req.method === 'DELETE') {
await deleteAccount(user_id);
return ok(res);
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,61 @@
import { getAccountById, getAccountByUsername, updateAccount, createAccount } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { hashPassword } from 'lib/crypto';
import { ok, unauthorized, methodNotAllowed, badRequest } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
const { user_id: current_user_id, is_admin: current_user_is_admin } = req.auth;
if (req.method === 'POST') {
const { user_id, username, password, is_admin } = req.body;
if (user_id) {
const account = await getAccountById(user_id);
if (account.user_id === current_user_id || current_user_is_admin) {
const data = {};
if (password) {
data.password = hashPassword(password);
}
// Only admin can change these fields
if (current_user_is_admin) {
// Cannot change username of admin
if (username !== 'admin') {
data.username = username;
}
data.is_admin = is_admin;
}
if (data.username && account.username !== data.username) {
const accountByUsername = await getAccountByUsername(username);
if (accountByUsername) {
return badRequest(res, 'Account already exists');
}
}
const updated = await updateAccount(user_id, data);
return ok(res, updated);
}
return unauthorized(res);
} else {
const accountByUsername = await getAccountByUsername(username);
if (accountByUsername) {
return badRequest(res, 'Account already exists');
}
const created = await createAccount({ username, password: hashPassword(password) });
return ok(res, created);
}
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,32 @@
import { getAccountById, updateAccount } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'lib/response';
import { checkPassword, hashPassword } from 'lib/crypto';
export default async (req, res) => {
await useAuth(req, res);
const { user_id: auth_user_id, is_admin } = req.auth;
const { user_id, current_password, new_password } = req.body;
if (!is_admin && user_id !== auth_user_id) {
return unauthorized(res);
}
if (req.method === 'POST') {
const account = await getAccountById(user_id);
const valid = checkPassword(current_password, account.password);
if (!valid) {
return badRequest(res, 'Current password is incorrect');
}
const password = hashPassword(new_password);
const updated = await updateAccount(user_id, { password });
return ok(res, updated);
}
return methodNotAllowed(res);
};

21
pages/api/accounts.js Normal file
View file

@ -0,0 +1,21 @@
import { getAccounts } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
const { is_admin } = req.auth;
if (!is_admin) {
return unauthorized(res);
}
if (req.method === 'GET') {
const accounts = await getAccounts();
return ok(res, accounts);
}
return methodNotAllowed(res);
};

32
pages/api/auth/login.js Normal file
View file

@ -0,0 +1,32 @@
import { serialize } from 'cookie';
import { checkPassword, createSecureToken } from 'lib/crypto';
import { getAccountByUsername } from 'lib/queries';
import { AUTH_COOKIE_NAME } from 'lib/constants';
import { ok, unauthorized, badRequest } from 'lib/response';
export default async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return badRequest(res);
}
const account = await getAccountByUsername(username);
if (account && (await checkPassword(password, account.password))) {
const { user_id, username, is_admin } = account;
const token = await createSecureToken({ user_id, username, is_admin });
const cookie = serialize(AUTH_COOKIE_NAME, token, {
path: '/',
httpOnly: true,
sameSite: true,
maxAge: 60 * 60 * 24 * 365,
});
res.setHeader('Set-Cookie', [cookie]);
return ok(res, { token });
}
return unauthorized(res);
};

15
pages/api/auth/logout.js Normal file
View file

@ -0,0 +1,15 @@
import { serialize } from 'cookie';
import { AUTH_COOKIE_NAME } from 'lib/constants';
import { ok } from 'lib/response';
export default async (req, res) => {
const cookie = serialize(AUTH_COOKIE_NAME, '', {
path: '/',
httpOnly: true,
maxAge: 0,
});
res.setHeader('Set-Cookie', [cookie]);
return ok(res);
};

12
pages/api/auth/verify.js Normal file
View file

@ -0,0 +1,12 @@
import { useAuth } from 'lib/middleware';
import { ok, unauthorized } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
if (req.auth) {
return ok(res, req.auth);
}
return unauthorized(res);
};

60
pages/api/collect.js Normal file
View file

@ -0,0 +1,60 @@
import isbot from 'isbot';
import ipaddr from 'ipaddr.js';
import { savePageView, saveEvent } from 'lib/queries';
import { useCors, useSession } from 'lib/middleware';
import { getIpAddress } from 'lib/request';
import { ok, badRequest } from 'lib/response';
import { createToken } from 'lib/crypto';
export default async (req, res) => {
await useCors(req, res);
if (isbot(req.headers['user-agent'])) {
return ok(res);
}
if (process.env.IGNORE_IP) {
const ips = process.env.IGNORE_IP.split(',').map(n => n.trim());
const ip = getIpAddress(req);
const blocked = ips.find(i => {
if (i === ip) return true;
// CIDR notation
if (i.indexOf('/') > 0) {
const addr = ipaddr.parse(ip);
const range = ipaddr.parseCIDR(i);
if (addr.kind() === range[0].kind() && addr.match(range)) return true;
}
return false;
});
if (blocked) {
return ok(res);
}
}
await useSession(req, res);
const { type, payload } = req.body;
const {
session: { website_id, session_id },
} = req;
if (type === 'pageview') {
const { url, referrer } = payload;
await savePageView(website_id, session_id, url, referrer);
} else if (type === 'event') {
const { url, event_type, event_value } = payload;
await saveEvent(website_id, session_id, url, event_type, event_value);
} else {
return badRequest(res);
}
const token = await createToken({ website_id, session_id });
return ok(res, token);
};

View file

@ -0,0 +1,26 @@
import { subMinutes } from 'date-fns';
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed } from 'lib/response';
import { getUserWebsites, getRealtimeData } from 'lib/queries';
import { createToken } from 'lib/crypto';
export default async (req, res) => {
await useAuth(req, res);
if (req.method === 'GET') {
const { user_id } = req.auth;
const websites = await getUserWebsites(user_id);
const ids = websites.map(({ website_id }) => website_id);
const token = await createToken({ websites: ids });
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
return ok(res, {
websites,
token,
data,
});
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,27 @@
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed, badRequest } from 'lib/response';
import { getRealtimeData } from 'lib/queries';
import { parseToken } from 'lib/crypto';
import { TOKEN_HEADER } from 'lib/constants';
export default async (req, res) => {
await useAuth(req, res);
if (req.method === 'GET') {
const { start_at } = req.query;
const token = req.headers[TOKEN_HEADER];
if (!token) {
return badRequest(res);
}
const { websites } = await parseToken(token);
const data = await getRealtimeData(websites, new Date(+start_at));
return ok(res, data);
}
return methodNotAllowed(res);
};

22
pages/api/share/[id].js Normal file
View file

@ -0,0 +1,22 @@
import { getWebsiteByShareId } from 'lib/queries';
import { ok, notFound, methodNotAllowed } from 'lib/response';
import { createToken } from 'lib/crypto';
export default async (req, res) => {
const { id } = req.query;
if (req.method === 'GET') {
const website = await getWebsiteByShareId(id);
if (website) {
const websiteId = website.website_id;
const token = await createToken({ website_id: websiteId });
return ok(res, { websiteId, token });
}
return notFound(res);
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,21 @@
import { getActiveVisitors } from 'lib/queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id } = req.query;
const websiteId = +id;
const result = await getActiveVisitors(websiteId);
return ok(res, result);
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,33 @@
import moment from 'moment-timezone';
import { getEventMetrics } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
const unitTypes = ['year', 'month', 'hour', 'day'];
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, start_at, end_at, unit, tz, url, event_type } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res);
}
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, {
url,
event_type,
});
return ok(res, events);
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,31 @@
import { deleteWebsite, getWebsiteById } from 'lib/queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
const { id } = req.query;
const websiteId = +id;
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const website = await getWebsiteById(websiteId);
return ok(res, website);
}
if (req.method === 'DELETE') {
if (!(await allowQuery(req, true))) {
return unauthorized(res);
}
await deleteWebsite(websiteId);
return ok(res);
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,74 @@
import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'lib/queries';
import { ok, methodNotAllowed, unauthorized, badRequest } from 'lib/response';
import { allowQuery } from 'lib/auth';
const sessionColumns = ['browser', 'os', 'device', 'country'];
const pageviewColumns = ['url', 'referrer'];
function getTable(type) {
if (type === 'event') {
return 'event';
}
if (sessionColumns.includes(type)) {
return 'session';
}
return 'pageview';
}
function getColumn(type) {
if (type === 'event') {
return `concat(event_type, '\t', event_value)`;
}
return type;
}
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, type, start_at, end_at, url } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
if (sessionColumns.includes(type)) {
const data = await getSessionMetrics(websiteId, startDate, endDate, type, { url });
return ok(res, data);
}
if (pageviewColumns.includes(type) || type === 'event') {
let domain;
if (type === 'referrer') {
const website = getWebsiteById(websiteId);
if (!website) {
return badRequest(res);
}
domain = website.domain;
}
const data = await getPageviewMetrics(
websiteId,
startDate,
endDate,
getColumn(type),
getTable(type),
{
domain,
url: type !== 'url' && url,
},
);
return ok(res, data);
}
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,36 @@
import moment from 'moment-timezone';
import { getPageviewStats } from 'lib/queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
const unitTypes = ['year', 'month', 'hour', 'day'];
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, start_at, end_at, unit, tz, url, ref } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
return badRequest(res);
}
const [pageviews, sessions] = await Promise.all([
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', { url, ref }),
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct session_id', {
url,
ref,
}),
]);
return ok(res, { pageviews, sessions });
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,20 @@
import { resetWebsite } from 'lib/queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
const { id } = req.query;
const websiteId = +id;
if (req.method === 'POST') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
await resetWebsite(websiteId);
return ok(res);
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,36 @@
import { getWebsiteStats } from 'lib/queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {
if (req.method === 'GET') {
if (!(await allowQuery(req))) {
return unauthorized(res);
}
const { id, start_at, end_at, url, ref } = req.query;
const websiteId = +id;
const startDate = new Date(+start_at);
const endDate = new Date(+end_at);
const distance = end_at - start_at;
const prevStartDate = new Date(+start_at - distance);
const prevEndDate = new Date(+end_at - distance);
const metrics = await getWebsiteStats(websiteId, startDate, endDate, { url, ref });
const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, { url, ref });
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
obj[key] = {
value: Number(metrics[0][key]) || 0,
change: Number(metrics[0][key] - prevPeriod[0][key]) || 0,
};
return obj;
}, {});
return ok(res, stats);
}
return methodNotAllowed(res);
};

View file

@ -0,0 +1,43 @@
import { updateWebsite, createWebsite, getWebsiteById } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { uuid, getRandomChars } from 'lib/crypto';
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
const { user_id, is_admin } = req.auth;
const { website_id, enable_share_url } = req.body;
if (req.method === 'POST') {
const { name, domain } = req.body;
if (website_id) {
const website = await getWebsiteById(website_id);
if (website.user_id !== user_id && !is_admin) {
return unauthorized(res);
}
let { share_id } = website;
if (enable_share_url) {
share_id = share_id ? share_id : getRandomChars(8);
} else {
share_id = null;
}
await updateWebsite(website_id, { name, domain, share_id });
return ok(res);
} else {
const website_uuid = uuid();
const share_id = enable_share_url ? getRandomChars(8) : null;
const website = await createWebsite(user_id, { website_uuid, name, domain, share_id });
return ok(res, website);
}
}
return methodNotAllowed(res);
};

23
pages/api/websites.js Normal file
View file

@ -0,0 +1,23 @@
import { getUserWebsites } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed, unauthorized } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
const { user_id: current_user_id, is_admin } = req.auth;
const { user_id } = req.query;
const userId = +user_id;
if (req.method === 'GET') {
if (userId && userId !== current_user_id && !is_admin) {
return unauthorized(res);
}
const websites = await getUserWebsites(userId || current_user_id);
return ok(res, websites);
}
return methodNotAllowed(res);
};