Accounts and login.

This commit is contained in:
Mike Cao 2020-07-23 19:56:55 -07:00
parent f3f0ad15f2
commit 49a55b40b4
22 changed files with 347 additions and 172 deletions

5
components/Footer.js Normal file
View File

@ -0,0 +1,5 @@
import React from 'react';
export default function Footer() {
return <footer className="container mt-5 mb-5">umami - deliciously simple web stats</footer>;
}

14
components/Header.js Normal file
View File

@ -0,0 +1,14 @@
import React from 'react';
import Link from 'next/link';
export default function Header() {
return (
<header className="container">
<h1>
<Link href="/">
<a>umami</a>
</Link>
</h1>
</header>
);
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import Head from 'next/head';
import Header from 'components/header';
import Footer from 'components/footer';
import Header from 'components/Header';
import Footer from 'components/Footer';
export default function Layout({ title, children }) {
return (
@ -13,14 +13,6 @@ export default function Layout({ title, children }) {
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400&display=swap"
rel="stylesheet"
/>
{typeof window !== 'undefined' && (
<script
async
defer
data-website-id="d0059975-b79a-4f83-8926-ed731475fded"
src="/umami.js"
/>
)}
</Head>
<Header />
<main className="container">{children}</main>

59
components/Login.js Normal file
View File

@ -0,0 +1,59 @@
import React, { useState } from 'react';
import { Formik, Form, Field, ErrorMessage } from 'formik';
import Router from 'next/router';
import { post } from 'lib/web';
const validate = ({ username, password }) => {
const errors = {};
if (!username) {
errors.username = 'Required';
}
if (!password) {
errors.password = 'Required';
}
return errors;
};
export default function Login() {
const [message, setMessage] = useState();
const handleSubmit = async ({ username, password }) => {
const response = await post('/api/auth', { username, password });
if (response?.token) {
await Router.push('/admin');
} else {
setMessage('Incorrect username/password.');
}
};
return (
<Formik
initialValues={{
username: '',
password: '',
}}
validate={validate}
onSubmit={handleSubmit}
>
{() => (
<Form>
<h3>{message}</h3>
<div>
<label htmlFor="username">Username</label>
<Field name="username" type="text" />
<ErrorMessage name="username" />
</div>
<div>
<label htmlFor="password">Password</label>
<Field name="password" type="password" />
<ErrorMessage name="password" />
</div>
<button type="submit">Submit</button>
</Form>
)}
</Formik>
);
}

View File

@ -1,5 +0,0 @@
import React from 'react';
export default function Footer() {
return <footer className="container">Footer</footer>;
}

View File

@ -1,7 +0,0 @@
import React from 'react';
export default function Header() {
return <header className="container">
<h1>umami</h1>
</header>;
}

3
index.js Normal file
View File

@ -0,0 +1,3 @@
const { name, version } = require('./package.json');
console.log(`${name} ${version}`);

View File

@ -6,23 +6,19 @@ import { JWT, JWE, JWK } from 'jose';
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/;
const KEY = JWK.asKey(Buffer.from(secret()));
export function sha256(...args) {
return crypto.createHash('sha256').update(args.join('')).digest('hex');
export function hash(...args) {
return crypto.createHash('sha512').update(args.join('')).digest('hex');
}
export function secret() {
return sha256(process.env.HASH_SALT);
return hash(process.env.HASH_SALT);
}
export function uuid(...args) {
return v5(args.join(''), v5(process.env.HASH_SALT, v5.DNS));
}
export function random(n = 64) {
return crypto.randomBytes(n).toString('hex');
}
export function isValidHash(s) {
export function isValidId(s) {
return UUID_REGEX.test(s);
}

View File

@ -1,12 +1,12 @@
import { getWebsite, getSession, createSession } from 'lib/db';
import { getCountry, getDevice, getIpAddress } from 'lib/utils';
import { uuid, isValidHash, verifyToken } from 'lib/crypto';
import { uuid, isValidId, verifyToken } from 'lib/crypto';
export default async req => {
const { payload } = req.body;
const { website: website_uuid, hostname, screen, language, session } = payload;
if (!isValidHash(website_uuid)) {
if (!isValidId(website_uuid)) {
throw new Error(`Invalid website: ${website_uuid}`);
}

10
lib/web.js Normal file
View File

@ -0,0 +1,10 @@
export const post = (url, params) =>
fetch(url, {
method: 'post',
cache: 'no-cache',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
}).then(res => (res.status === 200 ? res.json() : null));

View File

@ -1,17 +1,23 @@
{
"name": "umami",
"version": "0.1.0",
"description": "Deliciously simple website analytics",
"description": "Deliciously simple website stats",
"main": "index.js",
"author": "Mike Cao <mike@mikecao.com>",
"license": "MIT",
"homepage": "https://github.com/mikecao/umami",
"repository": {
"type": "git",
"url": "https://github.com/mikecao/umami.git"
},
"scripts": {
"dev": "next dev -p 8000",
"build": "next build",
"start": "next start",
"build-script": "rollup -c",
"build-schema": "prisma introspect",
"build-client": "prisma generate"
"build-client": "prisma generate",
"create-account": "node scripts/create-account.js"
},
"lint-staged": {
"**/*.js": [
@ -29,7 +35,6 @@
},
"dependencies": {
"@prisma/client": "2.2.2",
"base64url": "^3.0.1",
"bcrypt": "^5.0.0",
"chart.js": "^2.9.3",
"classnames": "^2.2.6",
@ -38,12 +43,13 @@
"date-fns": "^2.14.0",
"detect-browser": "^5.1.1",
"dotenv": "^8.2.0",
"formik": "^2.1.5",
"geolite2-redist": "^1.0.7",
"is-localhost-ip": "^1.4.0",
"jose": "^1.27.2",
"jsonwebtoken": "^8.5.1",
"maxmind": "^4.1.3",
"next": "9.3.5",
"next-cookies": "^2.0.3",
"node-fetch": "^2.6.0",
"promise-polyfill": "^8.1.3",
"react": "16.13.1",

View File

@ -1,10 +1,10 @@
import React from 'react';
import Layout from 'components/layout';
import Layout from 'components/Layout';
export default function Custom404() {
return (
<Layout title="404 - Page Not Found">
<h1>oops</h1>
<Layout>
<h1>oops! not found</h1>
</Layout>
);
}

36
pages/admin.js Normal file
View File

@ -0,0 +1,36 @@
import React from 'react';
import cookies from 'next-cookies';
import Layout from 'components/Layout';
import { verifySecureToken } from 'lib/crypto';
export default function Admin({ username }) {
return (
<Layout title="Admin">
<h2>
You've successfully logged in as <b>{username}</b>.
</h2>
</Layout>
);
}
export async function getServerSideProps(context) {
const token = cookies(context)['umami.auth'];
try {
const payload = await verifySecureToken(token);
return {
props: {
username: payload.username,
},
};
} catch {
const { res } = context;
res.statusCode = 303;
res.setHeader('Location', '/');
res.end();
}
return { props: {} };
}

View File

@ -1,8 +1,11 @@
import { serialize } from 'cookie';
import { checkPassword, createSecureToken } from 'lib/crypto';
import { getAccount } from 'lib/db';
import { allowPost } from 'lib/middleware';
export default async (req, res) => {
await allowPost(req, res);
const { username, password } = req.body;
const account = await getAccount(username);
@ -10,13 +13,16 @@ export default async (req, res) => {
if (account && (await checkPassword(password, account.password))) {
const { user_id, username, is_admin } = account;
const token = await createSecureToken({ user_id, username, is_admin });
const expires = new Date(Date.now() + 31536000000);
const cookie = serialize('umami.auth', token, { expires, httpOnly: true });
const cookie = serialize('umami.auth', token, {
path: '/',
httpOnly: true,
maxAge: 60 * 60 * 24 * 365,
});
res.setHeader('Set-Cookie', [cookie]);
res.status(200).send({ token });
} else {
res.status(401).send('');
res.status(401).end();
}
};

View File

@ -8,25 +8,24 @@ export default async (req, res) => {
const session = await checkSession(req);
const token = await createToken(session);
const { website_id, session_id } = session;
const { type, payload } = req.body;
let ok = 1;
let ok = false;
if (type === 'pageview') {
const { url, referrer } = payload;
await savePageView(website_id, session_id, url, referrer).catch(e => {
ok = 0;
throw e;
});
await savePageView(website_id, session_id, url, referrer);
ok = true;
} else if (type === 'event') {
const { url, event_type, event_value } = payload;
await saveEvent(website_id, session_id, url, event_type, event_value).catch(() => {
ok = 0;
throw e;
});
}
const token = await createToken(session);
await saveEvent(website_id, session_id, url, event_type, event_value);
ok = true;
}
res.status(200).json({ ok, session: token });
};

View File

@ -7,6 +7,6 @@ export default async (req, res) => {
const payload = await verifySecureToken(token);
res.status(200).send(payload);
} catch {
res.status(400).send('');
res.status(400).end();
}
};

View File

@ -1,23 +1,17 @@
import React from 'react';
import Layout from 'components/layout';
import Link from 'next/link';
import Layout from 'components/Layout';
import Login from 'components/Login';
export default function Home() {
return (
<Layout>
Hello.
<br />
<Link href="/?q=abc">
<a>abc</a>
</Link>
<br />
<Link href="/?q=123">
<a>123</a>
</Link>
<br />
<button id="primary-button" className="otherClass umami--click--primary-button" type="button">
Button
</button>
<Login />
<p>
<Link href="/test">
<a>Test page 🡒</a>
</Link>
</p>
</Layout>
);
}

46
pages/test.js Normal file
View File

@ -0,0 +1,46 @@
import Head from 'next/head';
import Link from 'next/link';
import Layout from 'components/Layout';
export default function Test({ websiteId }) {
return (
<>
<Head>
{typeof window !== 'undefined' && (
<script async defer data-website-id={websiteId} src="/umami.js" />
)}
</Head>
<Layout>
<p>
Here you can test if your umami installation works. Open the network tab in your browser's
developer console and watch for requests to the url <b>collect</b>. The links below should
trigger page views. Clicking on the button should trigger an event.
</p>
<h2>Page links</h2>
<Link href="?q=1">
<a>Page One</a>
</Link>
<br />
<Link href="?q=2">
<a>Page Two</a>
</Link>
<h2>Events</h2>
<button
id="primary-button"
className="otherClass umami--click--primary-button"
type="button"
>
Button
</button>
</Layout>
</>
);
}
export async function getStaticProps() {
return {
props: {
websiteId: process.env.TEST_WEBSITE_ID,
},
};
}

28
scripts/create-account.js Normal file
View File

@ -0,0 +1,28 @@
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
const exec = async () => {
const account = await prisma.account.findOne({
where: {
username: 'admin',
},
});
if (!account) {
await prisma.account.create({
data: {
username: 'admin',
password: '$2a$10$BXHPV7APlV1I6WrKJt1igeJAyVsvbhMTaTAi3nHkUJFGPsYmfZq3y',
is_admin: true,
},
});
console.log('Account succesfully created.');
} else {
console.log('Account already exists.');
}
process.exit(0);
};
exec();

View File

@ -1,5 +1,6 @@
import 'promise-polyfill/src/polyfill';
import 'unfetch/polyfill';
import { post } from 'lib/web';
((window, sessionKey) => {
const {
@ -20,19 +21,6 @@ import 'unfetch/polyfill';
let currentUrl = `${pathname}${search}`;
let currentRef = document.referrer;
/* Helper methods */
const post = (url, params) =>
fetch(url, {
method: 'post',
cache: 'no-cache',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify(params),
}).then(res => res.json());
const collect = (type, params) => {
const payload = {
session: store.getItem(sessionKey),
@ -53,7 +41,7 @@ import 'unfetch/polyfill';
return post(`${hostUrl}/api/collect`, {
type,
payload,
}).then(({ ok, session }) => ok && session && store.setItem(sessionKey, session));
}).then(({ session }) => session && store.setItem(sessionKey, session));
};
const pageView = () => collect('pageview').then(() => setTimeout(loadEvents, 300));

View File

@ -1,8 +1,8 @@
html,
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, sans-serif;
font-size: 17px;
font-weight: 300;
font-size: 16px;
font-weight: 400;
line-height: 1.8;
padding: 0;
margin: 0;
@ -22,4 +22,18 @@ body {
flex-direction: column;
width: 100%;
height: 100%;
}
}
header a {
color: #000;
text-decoration: none;
}
form label {
display: inline-block;
min-width: 100px;
}
form input {
margin-right: 10px;
}

165
yarn.lock
View File

@ -1361,6 +1361,11 @@
resolved "https://registry.yarnpkg.com/@types/color-name/-/color-name-1.1.1.tgz#1c1261bbeaa10a8055bbc5d8ab84b7b2afc846a0"
integrity sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==
"@types/cookie@^0.3.3":
version "0.3.3"
resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.3.3.tgz#85bc74ba782fb7aa3a514d11767832b0e3bc6803"
integrity sha512-LKVP3cgXBT9RYj+t+9FDKwS5tdI+rPBXaNSkma7hvqy35lc7mAokC2zsqWJH0LaqIt3B962nuYI77hsJoT1gow==
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -1391,6 +1396,11 @@
resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e"
integrity sha512-f5j5b/Gf71L+dbqxIpQ4Z2WlmI/mPJ0fOkGGmFgtb6sAu97EPczzbS3/tJKxmcYDj55OX6ssqwDAWOHIYDRDGA==
"@types/object-assign@^4.0.30":
version "4.0.30"
resolved "https://registry.yarnpkg.com/@types/object-assign/-/object-assign-4.0.30.tgz#8949371d5a99f4381ee0f1df0a9b7a187e07e652"
integrity sha1-iUk3HVqZ9Dge4PHfCpt6GH4H5lI=
"@types/parse-json@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0"
@ -1942,11 +1952,6 @@ base64-js@^1.0.2:
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.3.1.tgz#58ece8cb75dd07e71ed08c736abc5fac4dbf8df1"
integrity sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==
base64url@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/base64url/-/base64url-3.0.1.tgz#6399d572e2bc3f90a9a8b22d5dbb0a32d33f788d"
integrity sha512-ir1UPr3dkwexU7FdV8qBBbNDRUhMmIekYMFZfi+C/sLNnRESKPl23nB9b2pltqfOQNnGzsDdId90AEtG5tCx4A==
base@^0.11.1:
version "0.11.2"
resolved "https://registry.yarnpkg.com/base/-/base-0.11.2.tgz#7bde5ced145b6d551a90db87f83c558b4eb48a8f"
@ -2139,11 +2144,6 @@ 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 sha1-+OcRMvf/5uAaXJaXpMbz5I1cyBk=
buffer-from@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef"
@ -2635,7 +2635,7 @@ convert-source-map@^0.3.3:
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-0.3.5.tgz#f1d802950af7dd2631a1febe0596550c86ab3190"
integrity sha1-8dgClQr33SYxof6+BZZVDIarMZA=
cookie@^0.4.1:
cookie@^0.4.0, cookie@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.4.1.tgz#afd713fe26ebd21ba95ceb61f9a8116e50a537d1"
integrity sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==
@ -3040,6 +3040,11 @@ deep-is@^0.1.3, deep-is@~0.1.3:
resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34"
integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=
deepmerge@^2.1.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-2.2.1.tgz#5d3ff22a01c00f645405a2fbc17d0778a1801170"
integrity sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==
deepmerge@^4.2.2:
version "4.2.2"
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.2.2.tgz#44d2ea3679b8f4d4ffba33f03d865fc1e7bf4955"
@ -3217,13 +3222,6 @@ duplexify@^3.4.2, duplexify@^3.6.0:
readable-stream "^2.0.0"
stream-shift "^1.0.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.3.322, electron-to-chromium@^1.3.488:
version "1.3.496"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.496.tgz#3f43d32930481d82ad3663d79658e7c59a58af0b"
@ -3860,6 +3858,20 @@ fork-ts-checker-webpack-plugin@3.1.1:
tapable "^1.0.0"
worker-rpc "^0.1.0"
formik@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/formik/-/formik-2.1.5.tgz#de5bbbe35543fa6d049fe96b8ee329d6cd6892b8"
integrity sha512-bWpo3PiqVDYslvrRjTq0Isrm0mFXHiO33D8MS6t6dWcqSFGeYF52nlpCM2xwOJ6tRVRznDkL+zz/iHPL4LDuvQ==
dependencies:
deepmerge "^2.1.1"
hoist-non-react-statics "^3.3.0"
lodash "^4.17.14"
lodash-es "^4.17.14"
react-fast-compare "^2.0.1"
scheduler "^0.18.0"
tiny-warning "^1.0.2"
tslib "^1.10.0"
fragment-cache@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
@ -4171,6 +4183,13 @@ hmac-drbg@^1.0.0:
minimalistic-assert "^1.0.0"
minimalistic-crypto-utils "^1.0.1"
hoist-non-react-statics@^3.3.0:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
dependencies:
react-is "^16.7.0"
hosted-git-info@^2.1.4:
version "2.8.8"
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
@ -4814,22 +4833,6 @@ json5@^2.1.0, json5@^2.1.2:
dependencies:
minimist "^1.2.5"
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"
jsx-ast-utils@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-2.4.1.tgz#1114a4c1209481db06c690c2b4f488cc665f657e"
@ -4838,23 +4841,6 @@ jsx-ast-utils@^2.4.1:
array-includes "^3.1.1"
object.assign "^4.1.0"
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"
kind-of@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-2.0.1.tgz#018ec7a4ce7e3a86cb9141be519d24c8faa981b5"
@ -5031,41 +5017,16 @@ locate-path@^5.0.0:
dependencies:
p-locate "^4.1.0"
lodash-es@^4.17.14:
version "4.17.15"
resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.15.tgz#21bd96839354412f23d7a10340e5eac6ee455d78"
integrity sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ==
lodash._reinterpolate@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d"
integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0=
lodash.includes@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f"
integrity sha1-YLuYqHy5I8aMoeUTJUgzFISfVT8=
lodash.isboolean@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz#6c2e171db2a257cd96802fd43b01b20d5f5870f6"
integrity sha1-bC4XHbKiV82WgC/UOwGyDV9YcPY=
lodash.isinteger@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz#619c0af3d03f8b04c31f5882840b77b11cd68343"
integrity sha1-YZwK89A/iwTDH1iChAt3sRzWg0M=
lodash.isnumber@^3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz#3ce76810c5928d03352301ac287317f11c0b1ffc"
integrity sha1-POdoEMWSjQM1IwGsKHMX8RwLH/w=
lodash.isplainobject@^4.0.6:
version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
integrity sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=
lodash.isstring@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/lodash.isstring/-/lodash.isstring-4.0.1.tgz#d527dfb5456eca7cc9bb95d5daeaf88ba54a5451"
integrity sha1-1SfftUVuynzJu5XV2ur4i6VKVFE=
lodash.memoize@^4.1.2:
version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
@ -5076,11 +5037,6 @@ lodash.merge@^4.6.0:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash.once@^4.0.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.once/-/lodash.once-4.1.1.tgz#0dd3971213c7c56df880977d504c88fb471a97ac"
integrity sha1-DdOXEhPHxW34gJd9UEyI+0cal6w=
lodash.template@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
@ -5572,6 +5528,13 @@ neo-async@^2.5.0, neo-async@^2.6.1:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
next-cookies@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/next-cookies/-/next-cookies-2.0.3.tgz#5a3eabcb6afa9b4d4ade69dfaaad749d16cd4a9a"
integrity sha512-YVCQzwZx+sz+KqLO4y9niHH9jjz6jajlEQbAKfsYVT6DOfngb/0k5l6vFK4rmpExVug96pGag8OBsdSRL9FZhQ==
dependencies:
universal-cookie "^4.0.2"
next-tick@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
@ -7092,12 +7055,17 @@ react-dom@16.13.1:
prop-types "^15.6.2"
scheduler "^0.19.1"
react-fast-compare@^2.0.1:
version "2.0.4"
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
react-is@16.8.6:
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
react-is@^16.8.1:
react-is@^16.7.0, react-is@^16.8.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
@ -7554,6 +7522,14 @@ sax@^1.2.4, sax@~1.2.4:
resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
integrity sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==
scheduler@^0.18.0:
version "0.18.0"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.18.0.tgz#5901ad6659bc1d8f3fdaf36eb7a67b0d6746b1c4"
integrity sha512-agTSHR1Nbfi6ulI0kYNK0203joW2Y5W4po4l+v03tOoiJKpTBbxpNhWDvqc/4IcOw+KLmSiQLTasZ4cab2/UWQ==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
scheduler@^0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.19.1.tgz#4f3e2ed2c1a7d65681f4c854fa8c5a1ccb40f196"
@ -8371,6 +8347,11 @@ tiny-lru@7.0.6:
resolved "https://registry.yarnpkg.com/tiny-lru/-/tiny-lru-7.0.6.tgz#b0c3cdede1e5882aa2d1ae21cb2ceccf2a331f24"
integrity sha512-zNYO0Kvgn5rXzWpL0y3RS09sMK67eGaQj9805jlK9G6pSadfriTczzLHFXa/xcW4mIRfmlB9HyQ/+SgL0V1uow==
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@^0.0.33:
version "0.0.33"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9"
@ -8653,6 +8634,16 @@ unist-util-visit@^2.0.0:
unist-util-is "^4.0.0"
unist-util-visit-parents "^3.0.0"
universal-cookie@^4.0.2:
version "4.0.3"
resolved "https://registry.yarnpkg.com/universal-cookie/-/universal-cookie-4.0.3.tgz#c2fa59127260e6ad21ef3e0cdd66ad453cbc41f6"
integrity sha512-YbEHRs7bYOBTIWedTR9koVEe2mXrq+xdjTJZcoKJK/pQaE6ni28ak2AKXFpevb+X6w3iU5SXzWDiJkmpDRb9qw==
dependencies:
"@types/cookie" "^0.3.3"
"@types/object-assign" "^4.0.30"
cookie "^0.4.0"
object-assign "^4.1.1"
unquote@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/unquote/-/unquote-1.1.1.tgz#8fded7324ec6e88a0ff8b905e7c098cdc086d544"