mirror of
https://github.com/umami-software/umami.git
synced 2026-02-04 04:37:11 +01:00
Use next-basics package.
This commit is contained in:
parent
1a6af8fc41
commit
f4e0da481e
62 changed files with 255 additions and 373 deletions
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 () {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
13
lib/kafka.js
13
lib/kafka.js
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
19
lib/redis.js
19
lib/redis.js
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
|
|
|||
|
|
@ -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
8
lib/security.js
Normal 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}` } : {};
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
35
lib/url.js
35
lib/url.js
|
|
@ -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;
|
||||
}
|
||||
78
lib/web.js
78
lib/web.js
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue