Use next-basics package.

This commit is contained in:
Mike Cao 2022-08-28 20:20:54 -07:00
parent 1a6af8fc41
commit f4e0da481e
62 changed files with 255 additions and 373 deletions

View file

@ -1,12 +1,13 @@
import { parseSecureToken, parseToken } from './crypto';
import { parseSecureToken, parseToken } from 'next-basics';
import { SHARE_TOKEN_HEADER } from './constants';
import { getWebsiteById } from 'queries';
import { secret } from './crypto';
export async function getAuthToken(req) {
try {
const token = req.headers.authorization;
return parseSecureToken(token.split(' ')[1]);
return parseSecureToken(token.split(' ')[1], secret());
} catch {
return null;
}
@ -14,7 +15,7 @@ export async function getAuthToken(req) {
export async function isValidToken(token, validation) {
try {
const result = await parseToken(token);
const result = parseToken(token, secret());
if (typeof validation === 'object') {
return !Object.keys(validation).find(key => result[key] !== validation[key]);

View file

@ -12,13 +12,9 @@ export const CLICKHOUSE_DATE_FORMATS = {
year: '%Y-01-01',
};
const log = debug('clickhouse');
const log = debug('umami:clickhouse');
function getClient() {
if (!process.env.CLICKHOUSE_URL) {
return null;
}
const {
hostname,
port,
@ -149,13 +145,13 @@ function parseFilters(table, column, filters = {}, params = [], sessionKey = 'se
};
}
function replaceQuery(string, params = []) {
let formattedString = string;
function formatQuery(str, params = []) {
let formattedString = str;
params.forEach((a, i) => {
let replace = a;
params.forEach((param, i) => {
let replace = param;
if (typeof a === 'string' || a instanceof String) {
if (typeof param === 'string' || param instanceof String) {
replace = `'${replace}'`;
}
@ -165,11 +161,11 @@ function replaceQuery(string, params = []) {
return formattedString;
}
async function rawQuery(query, params = [], debug = false) {
let formattedQuery = replaceQuery(query, params);
async function rawQuery(query, params = []) {
let formattedQuery = formatQuery(query, params);
if (debug || process.env.LOG_QUERY) {
console.log(formattedQuery);
if (process.env.LOG_QUERY) {
log(formattedQuery);
}
return clickhouse.query(formattedQuery).toPromise();
@ -188,7 +184,7 @@ async function findFirst(data) {
}
// Initialization
const clickhouse = global[CLICKHOUSE] || getClient();
const clickhouse = process.env.CLICKHOUSE_URL && (global[CLICKHOUSE] || getClient());
export default {
client: clickhouse,
@ -199,8 +195,7 @@ export default {
getBetweenDates,
getFilterQuery,
parseFilters,
replaceQuery,
rawQuery,
findUnique,
findFirst,
rawQuery,
};

View file

@ -1,24 +1,15 @@
import crypto from 'crypto';
import { v4, v5, validate } from 'uuid';
import bcrypt from 'bcryptjs';
import { JWT, JWE, JWK } from 'jose';
import { v4, v5 } from 'uuid';
import { startOfMonth } from 'date-fns';
const SALT_ROUNDS = 10;
const KEY = JWK.asKey(Buffer.from(secret()));
const ROTATING_SALT = hash(startOfMonth(new Date()).toUTCString());
const CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
export function hash(...args) {
return crypto.createHash('sha512').update(args.join('')).digest('hex');
}
import { hash } from 'next-basics';
export function secret() {
return hash(process.env.HASH_SALT || process.env.DATABASE_URL);
}
export function salt() {
return v5(hash(secret(), ROTATING_SALT), v5.DNS);
const ROTATING_SALT = hash(startOfMonth(new Date()).toUTCString());
return hash([secret(), ROTATING_SALT]);
}
export function uuid(...args) {
@ -26,49 +17,3 @@ export function uuid(...args) {
return v5(args.join(''), salt());
}
export function isValidUuid(s) {
return validate(s);
}
export function getRandomChars(n) {
let s = '';
for (let i = 0; i < n; i++) {
s += CHARS[Math.floor(Math.random() * CHARS.length)];
}
return s;
}
export function hashPassword(password) {
return bcrypt.hashSync(password, SALT_ROUNDS);
}
export function checkPassword(password, hash) {
return bcrypt.compareSync(password, hash);
}
export async function createToken(payload) {
return JWT.sign(payload, KEY);
}
export async function parseToken(token) {
try {
return JWT.verify(token, KEY);
} catch {
return null;
}
}
export async function createSecureToken(payload) {
return JWE.encrypt(await createToken(payload), KEY);
}
export async function parseSecureToken(token) {
try {
const result = await JWE.decrypt(token, KEY);
return parseToken(result.toString());
} catch {
return null;
}
}

View file

@ -4,6 +4,7 @@ export const MYSQL = 'mysql';
export const CLICKHOUSE = 'clickhouse';
export const KAFKA = 'kafka';
export const KAFKA_PRODUCER = 'kafka-producer';
export const REDIS = 'redis';
// Fixes issue with converting bigint values
BigInt.prototype.toJSON = function () {

View file

@ -1,5 +1,3 @@
import { removeWWW } from './url';
export const urlFilter = data => {
const isValidUrl = url => {
return url !== '' && url !== null && !url.startsWith('#');
@ -49,7 +47,7 @@ export const refFilter = data => {
try {
const url = new URL(x);
id = removeWWW(url.hostname) || url.href;
id = url.hostname.replace('www', '') || url.href;
} catch {
id = '';
}
@ -94,11 +92,7 @@ export const paramFilter = data => {
return obj;
}, {});
const d = Object.keys(map).flatMap(key =>
return Object.keys(map).flatMap(key =>
Object.keys(map[key]).map(n => ({ x: `${key}=${n}`, p: key, v: n, y: map[key][n] })),
);
console.log({ map, d });
return d;
};

View file

@ -3,13 +3,9 @@ import dateFormat from 'dateformat';
import debug from 'debug';
import { KAFKA, KAFKA_PRODUCER } from 'lib/db';
const log = debug('kafka');
const log = debug('umami:kafka');
function getClient() {
if (!process.env.KAFKA_URL || !process.env.KAFKA_BROKER) {
return null;
}
const { username, password } = new URL(process.env.KAFKA_URL);
const brokers = process.env.KAFKA_BROKER.split(',');
@ -73,8 +69,11 @@ let kafka;
let producer;
(async () => {
kafka = global[KAFKA] || getClient();
producer = global[KAFKA_PRODUCER] || (await getProducer());
kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (global[KAFKA] || getClient());
if (kafka) {
producer = global[KAFKA_PRODUCER] || (await getProducer());
}
})();
export default {

View file

@ -1,19 +1,7 @@
import { createMiddleware, unauthorized, badRequest, serverError } from 'next-basics';
import cors from 'cors';
import { getSession } from './session';
import { getAuthToken } from './auth';
import { unauthorized, badRequest, serverError } from './response';
export function createMiddleware(middleware) {
return (req, res) =>
new Promise((resolve, reject) => {
middleware(req, res, result => {
if (result instanceof Error) {
return reject(result);
}
return resolve(result);
});
});
}
export const useCors = createMiddleware(cors());
@ -23,7 +11,9 @@ export const useSession = createMiddleware(async (req, res, next) => {
try {
session = await getSession(req);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
return serverError(res, e.message);
}

View file

@ -2,9 +2,8 @@ import { PrismaClient } from '@prisma/client';
import chalk from 'chalk';
import moment from 'moment-timezone';
import debug from 'debug';
import { PRISMA, MYSQL, POSTGRESQL } from 'lib/db';
import { PRISMA, MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
import { FILTER_IGNORED } from 'lib/constants';
import { getDatabaseType } from 'lib/db';
const MYSQL_DATE_FORMATS = {
minute: '%Y-%m-%d %H:%i:00',
@ -22,7 +21,7 @@ const POSTGRESQL_DATE_FORMATS = {
year: 'YYYY-01-01',
};
const log = debug('prisma');
const log = debug('umami:prisma');
const PRISMA_OPTIONS = {
log: [

View file

@ -1,10 +1,11 @@
import { createClient } from 'redis';
import { startOfMonth } from 'date-fns';
import { getSessions, getAllWebsites } from '/queries';
import debug from 'debug';
import { getSessions, getAllWebsites } from 'queries';
import { REDIS } from 'lib/db';
const log = debug('db:redis');
const REDIS = Symbol.for('redis');
const log = debug('umami:redis');
const INITIALIZED = 'redis:initialized';
async function getClient() {
const redis = new createClient({
@ -38,7 +39,7 @@ async function stageData() {
await addRedis(sessionUuids);
await addRedis(websiteIds);
await redis.set('initialized', 'initialized');
await redis.set(INITIALIZED, 1);
}
async function addRedis(ids) {
@ -52,12 +53,12 @@ async function addRedis(ids) {
let redis = null;
(async () => {
redis = global[REDIS] || (await getClient());
redis = process.env.REDIS_URL && (global[REDIS] || (await getClient()));
const value = await redis.get('initialized');
if (!value) {
await stageData();
if (redis) {
if (!(await redis.get(INITIALIZED))) {
await stageData();
}
}
})();

View file

@ -1,43 +0,0 @@
export function ok(res, data = {}) {
return json(res, data);
}
export function json(res, data = {}) {
return res.status(200).json(data);
}
export function send(res, data, type = 'text/plain') {
res.setHeader('Content-Type', type);
return res.status(200).send(data);
}
export function redirect(res, url) {
res.setHeader('Location', url);
return res.status(303).end();
}
export function badRequest(res, msg = '400 Bad Request') {
return res.status(400).end(msg);
}
export function unauthorized(res, msg = '401 Unauthorized') {
return res.status(401).end(msg);
}
export function forbidden(res, msg = '403 Forbidden') {
return res.status(403).end(msg);
}
export function notFound(res, msg = '404 Not Found') {
return res.status(404).end(msg);
}
export function methodNotAllowed(res, msg = '405 Method Not Allowed') {
res.status(405).end(msg);
}
export function serverError(res, msg = '500 Internal Server Error') {
res.status(500).end(msg);
}

8
lib/security.js Normal file
View file

@ -0,0 +1,8 @@
import { getItem } from 'next-basics';
import { AUTH_TOKEN } from './constants';
export function getAuthHeader() {
const token = getItem(AUTH_TOKEN);
return token ? { authorization: `Bearer ${token}` } : {};
}

View file

@ -1,4 +1,6 @@
import { isValidUuid, parseToken, uuid } from 'lib/crypto';
import { parseToken } from 'next-basics';
import { validate } from 'uuid';
import { uuid } from 'lib/crypto';
import redis from 'lib/redis';
import { getClientInfo, getJsonBody } from 'lib/request';
import { createSession, getSessionByUuid, getWebsiteByUuid } from 'queries';
@ -22,8 +24,8 @@ export async function getSession(req) {
const { website: website_uuid, hostname, screen, language } = payload;
if (!isValidUuid(website_uuid)) {
throw new Error(`Invalid website: ${website_uuid}`);
if (!validate(website_uuid)) {
return null;
}
let websiteId = null;
@ -52,7 +54,6 @@ export async function getSession(req) {
if (process.env.REDIS_URL) {
sessionCreated = (await redis.get(`session:${session_uuid}`)) !== null;
} else {
console.log('test');
session = await getSessionByUuid(session_uuid);
sessionCreated = !!session;
sessionId = session ? session.session_id : null;
@ -60,7 +61,6 @@ export async function getSession(req) {
if (!sessionCreated) {
try {
console.log('test2');
session = await createSession(websiteId, {
session_uuid,
hostname,

View file

@ -1,35 +0,0 @@
export function removeTrailingSlash(url) {
return url && url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url;
}
export function removeWWW(url) {
return url && url.length > 1 && url.startsWith('www.') ? url.slice(4) : url;
}
export function getQueryString(params = {}) {
const map = Object.keys(params).reduce((arr, key) => {
if (params[key] !== undefined) {
return arr.concat(`${key}=${encodeURIComponent(params[key])}`);
}
return arr;
}, []);
if (map.length) {
return `?${map.join('&')}`;
}
return '';
}
export function makeUrl(url, params) {
return `${url}${getQueryString(params)}`;
}
export function safeDecodeURI(s) {
try {
return decodeURI(s);
} catch (e) {
console.error(e);
}
return s;
}

View file

@ -1,78 +0,0 @@
import { makeUrl } from './url';
export const apiRequest = (method, url, body, headers) => {
return fetch(url, {
method,
cache: 'no-cache',
credentials: 'same-origin',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
...headers,
},
body,
}).then(res => {
if (res.ok) {
return res.json().then(data => ({ ok: res.ok, status: res.status, data }));
}
return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data }));
});
};
export const get = (url, params, headers) =>
apiRequest('get', makeUrl(url, params), undefined, headers);
export const del = (url, params, headers) =>
apiRequest('delete', makeUrl(url, params), undefined, headers);
export const post = (url, params, headers) =>
apiRequest('post', url, JSON.stringify(params), headers);
export const put = (url, params, headers) =>
apiRequest('put', url, JSON.stringify(params), headers);
export const hook = (_this, method, callback) => {
const orig = _this[method];
return (...args) => {
callback.apply(null, args);
return orig.apply(_this, args);
};
};
export const doNotTrack = () => {
const { doNotTrack, navigator, external } = window;
const msTrackProtection = 'msTrackingProtectionEnabled';
const msTracking = () => {
return external && msTrackProtection in external && external[msTrackProtection]();
};
const dnt = doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack || msTracking();
return dnt == '1' || dnt === 'yes';
};
export const setItem = (key, data, session) => {
if (typeof window !== 'undefined' && data) {
(session ? sessionStorage : localStorage).setItem(key, JSON.stringify(data));
}
};
export const getItem = (key, session) => {
if (typeof window !== 'undefined') {
const value = (session ? sessionStorage : localStorage).getItem(key);
if (value !== 'undefined') {
return JSON.parse(value);
}
}
};
export const removeItem = (key, session) => {
if (typeof window !== 'undefined') {
(session ? sessionStorage : localStorage).removeItem(key);
}
};