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

@ -4,7 +4,12 @@
"es2020": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:prettier/recommended", "next"],
"extends": [
"eslint:recommended",
"plugin:prettier/recommended",
"plugin:import/recommended",
"next"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
@ -12,7 +17,27 @@
"ecmaVersion": 11,
"sourceType": "module"
},
"settings": {
"import/resolver": {
"alias": {
"map": [
["assets", "./assets"],
["components", "./components"],
["db", "./db"],
["hooks", "./hooks"],
["lang", "./lang"],
["lib", "./lib"],
["public", "./public"],
["queries", "./queries"],
["store", "./store"],
["styles", "./styles"]
],
"extensions": [".ts", ".js", ".jsx", ".json"]
}
}
},
"rules": {
"no-console": "error",
"react/display-name": "off",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",

View File

@ -1,10 +1,10 @@
import React from 'react';
import Link from 'next/link';
import classNames from 'classnames';
import Link from 'next/link';
import { safeDecodeURI } from 'next-basics';
import usePageQuery from 'hooks/usePageQuery';
import { safeDecodeURI } from 'lib/url';
import Icon from './Icon';
import External from 'assets/arrow-up-right-from-square.svg';
import Icon from './Icon';
import styles from './FilterLink.module.css';
export default function FilterLink({ id, value, label, externalUrl }) {
@ -25,7 +25,7 @@ export default function FilterLink({ id, value, label, externalUrl }) {
</a>
</Link>
{externalUrl && (
<a href={externalUrl} target="_blank" rel="noreferrer noopener" className={styles.link}>
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
<Icon icon={<External />} className={styles.icon} />
</a>
)}

View File

@ -1,8 +1,8 @@
import { useState, useEffect, useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { setItem } from 'next-basics';
import ButtonLayout from 'components/layout/ButtonLayout';
import useStore, { checkVersion } from 'store/version';
import { setItem } from 'lib/web';
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
import Button from './Button';
import styles from './UpdateNotice.module.css';

View File

@ -1,6 +1,7 @@
import React, { useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { Formik, Form, Field } from 'formik';
import { setItem } from 'next-basics';
import { useRouter } from 'next/router';
import Button from 'components/common/Button';
import FormLayout, {
@ -11,7 +12,6 @@ import FormLayout, {
} from 'components/layout/FormLayout';
import Icon from 'components/common/Icon';
import useApi from 'hooks/useApi';
import { setItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants';
import { setUser } from 'store/app';
import Logo from 'assets/logo.svg';

View File

@ -9,7 +9,7 @@ import HamburgerButton from 'components/common/HamburgerButton';
import UpdateNotice from 'components/common/UpdateNotice';
import UserButton from 'components/settings/UserButton';
import { HOMEPAGE_URL } from 'lib/constants';
import useConfig from '/hooks/useConfig';
import useConfig from 'hooks/useConfig';
import useUser from 'hooks/useUser';
import Logo from 'assets/logo.svg';
import styles from './Header.module.css';

View File

@ -1,8 +1,8 @@
import React from 'react';
import classNames from 'classnames';
import { safeDecodeURI } from 'next-basics';
import Button from 'components/common/Button';
import Times from 'assets/times.svg';
import { safeDecodeURI } from 'lib/url';
import styles from './FilterTags.module.css';
export default function FilterTags({ params, onClick }) {

View File

@ -1,10 +1,10 @@
import { useState } from 'react';
import { useIntl, defineMessages } from 'react-intl';
import MetricsTable from './MetricsTable';
import { safeDecodeURI } from 'next-basics';
import Tag from 'components/common/Tag';
import FilterButtons from 'components/common/FilterButtons';
import { paramFilter } from 'lib/filters';
import { safeDecodeURI } from 'lib/url';
import FilterButtons from '../common/FilterButtons';
import MetricsTable from './MetricsTable';
const FILTER_COMBINED = 0;
const FILTER_RAW = 1;

View File

@ -23,8 +23,6 @@ export default function DashboardEdit({ websites }) {
const ordered = useMemo(() => sortArrayByMap(websites, order, 'website_id'), [websites, order]);
console.log({ order, ordered });
function handleWebsiteDrag({ destination, source }) {
if (!destination || destination.index === source.index) return;

View File

@ -28,8 +28,6 @@ export default function TestConsole() {
const website = data.find(({ website_id }) => website_id === +websiteId);
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value;
console.log({ websiteId, data, options, website });
function handleSelect(value) {
router.push(`/console/${value}`);
}

View File

@ -24,8 +24,6 @@ export default function WebsiteList({ websites, showCharts, limit }) {
const { websiteOrder } = useDashboard();
const { formatMessage } = useIntl();
console.log({ websiteOrder });
const ordered = useMemo(
() => sortArrayByMap(websites, websiteOrder, 'website_id'),
[websites, websiteOrder],

View File

@ -1,11 +1,11 @@
import React from 'react';
import { FormattedMessage } from 'react-intl';
import { useRouter } from 'next/router';
import { removeItem } from 'next-basics';
import MenuButton from 'components/common/MenuButton';
import Icon from 'components/common/Icon';
import User from 'assets/user.svg';
import styles from './UserButton.module.css';
import { removeItem } from 'lib/web';
import { AUTH_TOKEN } from 'lib/constants';
import useUser from 'hooks/useUser';

View File

@ -1,6 +1,6 @@
import { useCallback } from 'react';
import { useRouter } from 'next/router';
import { get, post, put, del, getItem } from 'lib/web';
import { get, post, put, del, getItem } from 'next-basics';
import { AUTH_TOKEN, SHARE_TOKEN_HEADER } from 'lib/constants';
import useStore from 'store/app';

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { get } from 'lib/web';
import { get } from 'next-basics';
import enUS from 'public/intl/country/en-US.json';
const countryNames = {

View File

@ -1,7 +1,7 @@
import { useCallback, useMemo } from 'react';
import { parseISO } from 'date-fns';
import { getDateRange } from 'lib/date';
import { getItem, setItem } from 'lib/web';
import { getItem, setItem } from 'next-basics';
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
import useForceUpdate from './useForceUpdate';
import useLocale from './useLocale';

View File

@ -8,7 +8,7 @@ export default function useFetch(url, options = {}, update = []) {
const [loading, setLoading] = useState(false);
const [count, setCount] = useState(0);
const { get } = useApi();
const { params = {}, headers = {}, disabled, delay = 0, interval, onDataLoad } = options;
const { params = {}, headers = {}, disabled = false, delay = 0, interval, onDataLoad } = options;
async function loadData(params) {
try {
@ -29,7 +29,9 @@ export default function useFetch(url, options = {}, update = []) {
onDataLoad?.(data);
} catch (e) {
// eslint-disable-next-line no-console
console.error(e);
setError(e);
} finally {
setLoading(false);
@ -44,7 +46,7 @@ export default function useFetch(url, options = {}, update = []) {
clearTimeout(id);
};
}
}, [url, !!disabled, count, ...update]);
}, [url, disabled, count, ...update]);
useEffect(() => {
if (interval && !disabled) {
@ -54,7 +56,7 @@ export default function useFetch(url, options = {}, update = []) {
clearInterval(id);
};
}
}, [interval, !!disabled]);
}, [interval, disabled]);
return { ...response, error, loading };
}

View File

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { get } from 'lib/web';
import { get } from 'next-basics';
import enUS from 'public/intl/language/en-US.json';
const languageNames = {

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { get, setItem } from 'lib/web';
import { get, setItem } from 'next-basics';
import { LOCALE_CONFIG } from 'lib/constants';
import { getDateLocale, getTextDirection } from 'lib/lang';
import useStore, { setLocale } from 'store/app';

View File

@ -1,6 +1,9 @@
import { useMemo } from 'react';
import { useRouter } from 'next/router';
import { getQueryString } from 'lib/url';
function getQueryString(params) {
return new URLSearchParams({ ...params }).toString();
}
export default function usePageQuery() {
const router = useRouter();

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import useStore, { setTheme } from 'store/app';
import { getItem, setItem } from 'lib/web';
import { getItem, setItem } from 'next-basics';
import { THEME_CONFIG } from 'lib/constants';
const selector = state => state.theme;

View File

@ -1,6 +1,6 @@
import { useState, useCallback } from 'react';
import { getTimezone } from 'lib/date';
import { getItem, setItem } from 'lib/web';
import { getItem, setItem } from 'next-basics';
import { TIMEZONE_CONFIG } from 'lib/constants';
export default function useTimezone() {

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);
}
};

View File

@ -58,7 +58,6 @@
"dependencies": {
"@fontsource/inter": "4.5.7",
"@prisma/client": "4.2.1",
"bcryptjs": "^2.4.3",
"chalk": "^4.1.1",
"chart.js": "^2.9.4",
"classnames": "^2.3.1",
@ -81,11 +80,11 @@
"is-docker": "^3.0.0",
"is-localhost-ip": "^1.4.0",
"isbot": "^3.4.5",
"jose": "2.0.5",
"kafkajs": "^2.1.0",
"maxmind": "^4.3.6",
"moment-timezone": "^0.5.33",
"next": "^12.2.5",
"next-basics": "^0.6.0",
"node-fetch": "^3.2.8",
"npm-run-all": "^4.1.5",
"prop-types": "^15.7.2",
@ -115,6 +114,8 @@
"eslint": "^7.32.0",
"eslint-config-next": "^12.2.4",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-alias": "^1.1.2",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-prettier": "^4.0.0",
"extract-react-intl-messages": "^4.1.1",
"husky": "^7.0.0",

View File

@ -1,6 +1,6 @@
import { getAccountById, deleteAccount } from 'queries';
import { useAuth } from 'lib/middleware';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
export default async (req, res) => {
await useAuth(req, res);

View File

@ -1,7 +1,6 @@
import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics';
import { getAccountById, getAccountByUsername, updateAccount, createAccount } from '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);

View File

@ -1,7 +1,13 @@
import { getAccountById, updateAccount } from 'queries';
import { useAuth } from 'lib/middleware';
import { badRequest, methodNotAllowed, ok, unauthorized } from 'lib/response';
import { checkPassword, hashPassword } from 'lib/crypto';
import {
badRequest,
methodNotAllowed,
ok,
unauthorized,
checkPassword,
hashPassword,
} from 'next-basics';
export default async (req, res) => {
await useAuth(req, res);

View File

@ -1,6 +1,6 @@
import { getAccounts } from 'queries';
import { useAuth } from 'lib/middleware';
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
import { ok, unauthorized, methodNotAllowed } from 'next-basics';
export default async (req, res) => {
await useAuth(req, res);

View File

@ -1,6 +1,6 @@
import { checkPassword, createSecureToken } from 'lib/crypto';
import { ok, unauthorized, badRequest, checkPassword, createSecureToken } from 'next-basics';
import { getAccountByUsername } from 'queries/admin/account/getAccountByUsername';
import { ok, unauthorized, badRequest } from 'lib/response';
import { secret } from 'lib/crypto';
export default async (req, res) => {
const { username, password } = req.body;
@ -11,10 +11,10 @@ export default async (req, res) => {
const account = await getAccountByUsername(username);
if (account && (await checkPassword(password, account.password))) {
if (account && checkPassword(password, account.password)) {
const { user_id, username, is_admin } = account;
const user = { user_id, username, is_admin };
const token = await createSecureToken(user);
const token = createSecureToken(user, secret());
return ok(res, { token, user });
}

View File

@ -1,5 +1,5 @@
import { useAuth } from 'lib/middleware';
import { ok, unauthorized } from 'lib/response';
import { ok, unauthorized } from 'next-basics';
export default async (req, res) => {
await useAuth(req, res);

View File

@ -1,12 +1,10 @@
const { Resolver } = require('dns').promises;
import isbot from 'isbot';
import ipaddr from 'ipaddr.js';
import { createToken, unauthorized, send, badRequest, forbidden } from 'next-basics';
import { savePageView, saveEvent } from 'queries';
import { useCors, useSession } from 'lib/middleware';
import { getJsonBody, getIpAddress } from 'lib/request';
import { unauthorized, send, badRequest, forbidden } from 'lib/response';
import { createToken } from 'lib/crypto';
import { removeTrailingSlash } from 'lib/url';
import { uuid } from 'lib/crypto';
export default async (req, res) => {
@ -69,7 +67,7 @@ export default async (req, res) => {
let { url, referrer, event_name, event_data } = payload;
if (process.env.REMOVE_TRAILING_SLASH) {
url = removeTrailingSlash(url);
url = url.replace(/\/$/, '');
}
const event_uuid = uuid();
@ -89,7 +87,7 @@ export default async (req, res) => {
return badRequest(res);
}
const token = await createToken({ website_id, session_id, session_uuid });
const token = createToken({ website_id, session_id, session_uuid });
return send(res, token);
};

View File

@ -1,4 +1,4 @@
import { ok, methodNotAllowed } from 'lib/response';
import { ok, methodNotAllowed } from 'next-basics';
export default async (req, res) => {
if (req.method === 'GET') {

View File

@ -1,4 +1,4 @@
import { ok } from 'lib/response';
import { ok } from 'next-basics';
export default async (req, res) => {
return ok(res, 'nice');

View File

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

View File

@ -1,8 +1,8 @@
import { ok, methodNotAllowed, badRequest, parseToken } from 'next-basics';
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed, badRequest } from 'lib/response';
import { getRealtimeData } from 'queries';
import { parseToken } from 'lib/crypto';
import { SHARE_TOKEN_HEADER } from 'lib/constants';
import { secret } from 'lib/crypto';
export default async (req, res) => {
await useAuth(req, res);
@ -16,7 +16,7 @@ export default async (req, res) => {
return badRequest(res);
}
const { websites } = await parseToken(token);
const { websites } = parseToken(token, secret());
const data = await getRealtimeData(websites, new Date(+start_at));

View File

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

View File

@ -1,4 +1,4 @@
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { allowQuery } from 'lib/auth';
import { useCors } from 'lib/middleware';
import { getActiveVisitors } from 'queries';

View File

@ -1,6 +1,6 @@
import moment from 'moment-timezone';
import { getEventMetrics } from 'queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
import { allowQuery } from 'lib/auth';
import { useCors } from 'lib/middleware';

View File

@ -1,5 +1,5 @@
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { deleteWebsite, getWebsiteById } from 'queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { allowQuery } from 'lib/auth';
import { useCors } from 'lib/middleware';

View File

@ -1,5 +1,5 @@
import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'queries';
import { ok, methodNotAllowed, unauthorized, badRequest } from 'lib/response';
import { ok, methodNotAllowed, unauthorized, badRequest } from 'next-basics';
import { allowQuery } from 'lib/auth';
import { useCors } from 'lib/middleware';
import { FILTER_IGNORED } from 'lib/constants';

View File

@ -1,6 +1,6 @@
import moment from 'moment-timezone';
import { getPageviewStats } from 'queries';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
import { allowQuery } from 'lib/auth';
import { useCors } from 'lib/middleware';

View File

@ -1,5 +1,5 @@
import { resetWebsite } from 'queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { allowQuery } from 'lib/auth';
export default async (req, res) => {

View File

@ -1,5 +1,5 @@
import { getWebsiteStats } from 'queries';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
import { allowQuery } from 'lib/auth';
import { useCors } from 'lib/middleware';

View File

@ -1,7 +1,7 @@
import { ok, unauthorized, methodNotAllowed, getRandomChars } from 'next-basics';
import { updateWebsite, createWebsite, getWebsiteById } from 'queries';
import { useAuth } from 'lib/middleware';
import { uuid, getRandomChars } from 'lib/crypto';
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
import { uuid } from 'lib/crypto';
export default async (req, res) => {
await useAuth(req, res);

View File

@ -1,6 +1,6 @@
import { getAllWebsites, getUserWebsites } from 'queries';
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed, unauthorized } from 'lib/response';
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
export default async (req, res) => {
await useAuth(req, res);

View File

@ -1,6 +1,6 @@
import { useEffect } from 'react';
import { useRouter } from 'next/router';
import { removeItem } from 'lib/web';
import { removeItem } from 'next-basics';
import { AUTH_TOKEN } from 'lib/constants';
import { setUser } from 'store/app';

View File

@ -1,11 +1,11 @@
/* eslint-disable no-console */
require('dotenv').config();
const bcrypt = require('bcryptjs');
const { hashPassword } = require('next-basics');
const chalk = require('chalk');
const prompts = require('prompts');
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const SALT_ROUNDS = 10;
const runQuery = async query => {
return query.catch(e => {
@ -24,10 +24,6 @@ const updateAccountByUsername = (username, data) => {
);
};
const hashPassword = password => {
return bcrypt.hashSync(password, SALT_ROUNDS);
};
const changePassword = async (username, newPassword) => {
const password = hashPassword(newPassword);
return updateAccountByUsername(username, { password });

View File

@ -1,6 +1,6 @@
import create from 'zustand';
import { DEFAULT_LOCALE, DEFAULT_THEME, LOCALE_CONFIG, THEME_CONFIG } from 'lib/constants';
import { getItem } from 'lib/web';
import { getItem } from 'next-basics';
const initialState = {
locale: getItem(LOCALE_CONFIG) || DEFAULT_LOCALE,

View File

@ -1,6 +1,6 @@
import create from 'zustand';
import { DASHBOARD_CONFIG, DEFAULT_WEBSITE_LIMIT } from 'lib/constants';
import { getItem, setItem } from 'lib/web';
import { getItem, setItem } from 'next-basics';
export const initialState = {
showCharts: true,

View File

@ -2,7 +2,7 @@ import create from 'zustand';
import produce from 'immer';
import semver from 'semver';
import { CURRENT_VERSION, VERSION_CHECK, UPDATES_URL } from 'lib/constants';
import { getItem } from 'lib/web';
import { getItem } from 'next-basics';
const initialState = {
current: CURRENT_VERSION,

107
yarn.lock
View File

@ -1440,11 +1440,6 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
"@panva/asn1.js@^1.0.0":
version "1.0.0"
resolved "https://registry.npmjs.org/@panva/asn1.js/-/asn1.js-1.0.0.tgz"
integrity sha512-UdkG3mLEqXgnlKsWanWcgb6dOjUzJ+XC5f+aWw30qrtjxeNUSfKX1cd5FBzOaXQumoe9nIqeZUvrRJS03HCCtw==
"@prisma/client@4.2.1":
version "4.2.1"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.2.1.tgz#b384587f6066070381ea4c90228a14697a0c271b"
@ -2373,6 +2368,11 @@ buble@^0.20.0:
minimist "^1.2.5"
regexpu-core "4.5.4"
buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
integrity sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==
buffer-from@^1.0.0:
version "1.1.2"
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
@ -3020,6 +3020,13 @@ ecc-jsbn@~0.1.1:
jsbn "~0.1.0"
safer-buffer "^2.1.0"
ecdsa-sig-formatter@1.0.11:
version "1.0.11"
resolved "https://registry.yarnpkg.com/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz#ae0f0fa2d85045ef14a817daa3ce9acd0489e5bf"
integrity sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==
dependencies:
safe-buffer "^5.0.1"
electron-to-chromium@^1.4.118:
version "1.4.143"
resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.143.tgz"
@ -3144,6 +3151,11 @@ eslint-config-prettier@^8.5.0:
resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz"
integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==
eslint-import-resolver-alias@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-alias/-/eslint-import-resolver-alias-1.1.2.tgz#297062890e31e4d6651eb5eba9534e1f6e68fc97"
integrity sha512-WdviM1Eu834zsfjHtcGHtGfcu+F30Od3V7I9Fi57uhBEwPkjDcii7/yW8jAT+gOhn4P/vOxxNAXbFAKsrrc15w==
eslint-import-resolver-node@^0.3.6:
version "0.3.6"
resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd"
@ -4146,13 +4158,6 @@ jest-worker@^26.2.1:
merge-stream "^2.0.0"
supports-color "^7.0.0"
jose@2.0.5:
version "2.0.5"
resolved "https://registry.npmjs.org/jose/-/jose-2.0.5.tgz"
integrity sha512-BAiDNeDKTMgk4tvD0BbxJ8xHEHBZgpeRZ1zGPPsitSyMgjoMWiLGYAE7H7NpP5h0lPppQajQs871E8NHUrzVPA==
dependencies:
"@panva/asn1.js" "^1.0.0"
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@ -4254,6 +4259,22 @@ jsonparse@^1.2.0:
resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280"
integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==
jsonwebtoken@^8.5.1:
version "8.5.1"
resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-8.5.1.tgz#00e71e0b8df54c2121a1f26137df2280673bcc0d"
integrity sha512-XjwVfRS6jTMsqYs0EsuJ4LGxXV14zQybNd4L2r0UvbVnSF9Af8x7p5MzbJ90Ioz/9TI41/hTCvznF/loiSzn8w==
dependencies:
jws "^3.2.2"
lodash.includes "^4.3.0"
lodash.isboolean "^3.0.3"
lodash.isinteger "^4.0.4"
lodash.isnumber "^3.0.3"
lodash.isplainobject "^4.0.6"
lodash.isstring "^4.0.1"
lodash.once "^4.0.0"
ms "^2.1.1"
semver "^5.6.0"
jsprim@^1.2.2:
version "1.4.2"
resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.2.tgz#712c65533a15c878ba59e9ed5f0e26d5b77c5feb"
@ -4272,6 +4293,23 @@ jsprim@^1.2.2:
array-includes "^3.1.5"
object.assign "^4.1.3"
jwa@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/jwa/-/jwa-1.4.1.tgz#743c32985cb9e98655530d53641b66c8645b039a"
integrity sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==
dependencies:
buffer-equal-constant-time "1.0.1"
ecdsa-sig-formatter "1.0.11"
safe-buffer "^5.0.1"
jws@^3.2.2:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jws/-/jws-3.2.2.tgz#001099f3639468c9414000e99995fa52fb478304"
integrity sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==
dependencies:
jwa "^1.4.1"
safe-buffer "^5.0.1"
kafkajs@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/kafkajs/-/kafkajs-2.2.0.tgz#43b2d13c82395acee4500f09d6c7d503db8c77ea"
@ -4395,6 +4433,36 @@ lodash.debounce@^4.0.8:
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==
lodash.merge@^4.6.2:
version "4.6.2"
resolved "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
@ -4405,6 +4473,11 @@ lodash.mergewith@^4.6.2:
resolved "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz"
integrity sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==
lodash.pick@^4.4.0:
version "4.4.0"
resolved "https://registry.npmjs.org/lodash.pick/-/lodash.pick-4.4.0.tgz"
@ -4662,6 +4735,14 @@ natural-compare@^1.4.0:
resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=
next-basics@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/next-basics/-/next-basics-0.6.0.tgz#bbb3b2dafa69931c3b7aad0cd456332ddcf019c7"
integrity sha512-S9deRGhQPj9tN9WSroK8UAcxFuoV38YNFO9B5qEQpt7ZUNCkAUITccW98LGlJ5WfNzkp7dnXVgmL3+yvRWlH4w==
dependencies:
bcryptjs "^2.4.3"
jsonwebtoken "^8.5.1"
next@^12.2.5:
version "12.2.5"
resolved "https://registry.yarnpkg.com/next/-/next-12.2.5.tgz#14fb5975e8841fad09553b8ef41fe1393602b717"
@ -5866,7 +5947,7 @@ semver-compare@^1.0.0:
resolved "https://registry.npmjs.org/semver-compare/-/semver-compare-1.0.0.tgz"
integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w=
"semver@2 || 3 || 4 || 5", semver@^5.5.0:
"semver@2 || 3 || 4 || 5", semver@^5.5.0, semver@^5.6.0:
version "5.7.1"
resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz"
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==