Refactor database queries.

This commit is contained in:
Mike Cao 2020-08-11 22:24:41 -07:00
parent a248f35db2
commit f4ca353b5c
24 changed files with 371 additions and 329 deletions

View File

@ -5,7 +5,6 @@ import Button from 'components/common/Button';
import Icon from 'components/common/Icon';
import Table from 'components/common/Table';
import Modal from 'components/common/Modal';
import WebsiteEditForm from 'components/forms/WebsiteEditForm';
import AccountEditForm from 'components/forms/AccountEditForm';
import Pen from 'assets/pen.svg';
import Plus from 'assets/plus.svg';
@ -16,33 +15,36 @@ import styles from './AccountSettings.module.css';
import DeleteForm from './forms/DeleteForm';
export default function AccountSettings() {
const user = useSelector(state => state.user);
const [data, setData] = useState();
const [addAccount, setAddAccount] = useState();
const [editAccount, setEditAccount] = useState();
const [deleteAccount, setDeleteAccount] = useState();
const [saved, setSaved] = useState(0);
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
const Buttons = row =>
row.username !== 'admin' ? (
<>
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
<div>Edit</div>
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
<div>Delete</div>
</Button>
</>
) : null;
const columns = [
{ key: 'username', label: 'Username' },
{
key: 'is_admin',
label: 'Administrator',
render: ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null),
render: Checkmark,
},
{
className: styles.buttons,
render: row =>
row.username !== 'admin' ? (
<>
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
<div>Edit</div>
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
<div>Delete</div>
</Button>
</>
) : null,
render: Buttons,
},
];
@ -58,7 +60,7 @@ export default function AccountSettings() {
}
async function loadData() {
setData(await get(`/api/account`));
setData(await get(`/api/accounts`));
}
useEffect(() => {

View File

@ -1,9 +1,9 @@
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import PageHeader from './layout/PageHeader';
import Button from './common/Button';
import PageHeader from 'components/layout/PageHeader';
import Button from 'components/common/Button';
import ChangePasswordForm from './forms/ChangePasswordForm';
import Modal from './common/Modal';
import Modal from 'components/common/Modal';
export default function ProfileSettings() {
const user = useSelector(state => state.user);

View File

@ -1,12 +1,12 @@
import React, { useEffect, useState } from 'react';
import classNames from 'classnames';
import WebsiteChart from './charts/WebsiteChart';
import RankingsChart from './charts/RankingsChart';
import WorldMap from './common/WorldMap';
import Page from './layout/Page';
import PageHeader from './layout/PageHeader';
import MenuLayout from './layout/MenuLayout';
import Button from './common/Button';
import WebsiteChart from 'components/charts/WebsiteChart';
import RankingsChart from 'components/charts/RankingsChart';
import WorldMap from 'components/common/WorldMap';
import Page from 'components/layout/Page';
import PageHeader from 'components/layout/PageHeader';
import MenuLayout from 'components/layout/MenuLayout';
import Button from 'components/common/Button';
import { getDateRange } from 'lib/date';
import { get } from 'lib/web';
import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters';

View File

@ -1,22 +1,21 @@
import React, { useState, useEffect } from 'react';
import { useRouter } from 'next/router';
import { get } from 'lib/web';
import Link from './common/Link';
import WebsiteChart from './charts/WebsiteChart';
import Page from './layout/Page';
import Icon from './common/Icon';
import Button from './common/Button';
import PageHeader from './layout/PageHeader';
import Link from 'components/common/Link';
import WebsiteChart from 'components/charts/WebsiteChart';
import Page from 'components/layout/Page';
import Button from 'components/common/Button';
import PageHeader from 'components/layout/PageHeader';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Arrow from 'assets/arrow-right.svg';
import { get } from 'lib/web';
import styles from './WebsiteList.module.css';
import EmptyPlaceholder from './common/EmptyPlaceholder';
export default function WebsiteList() {
const [data, setData] = useState();
const router = useRouter();
async function loadData() {
setData(await get(`/api/website`));
setData(await get(`/api/websites`));
}
useEffect(() => {

View File

@ -1,19 +1,18 @@
import React, { useState, useEffect } from 'react';
import Table from './common/Table';
import Button from './common/Button';
import PageHeader from './layout/PageHeader';
import Table from 'components/common/Table';
import Button from 'components/common/Button';
import PageHeader from 'components/layout/PageHeader';
import Modal from 'components/common/Modal';
import WebsiteEditForm from './forms/WebsiteEditForm';
import DeleteForm from './forms/DeleteForm';
import WebsiteCodeForm from './forms/WebsiteCodeForm';
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
import Pen from 'assets/pen.svg';
import Trash from 'assets/trash.svg';
import Plus from 'assets/plus.svg';
import Code from 'assets/code.svg';
import { get } from 'lib/web';
import Modal from './common/Modal';
import WebsiteEditForm from './forms/WebsiteEditForm';
import DeleteForm from './forms/DeleteForm';
import WebsiteCodeForm from './forms/WebsiteCodeForm';
import styles from './WebsiteSettings.module.css';
import EmptyPlaceholder from './common/EmptyPlaceholder';
import Arrow from '../assets/arrow-right.svg';
export default function WebsiteSettings() {
const [data, setData] = useState();
@ -23,25 +22,27 @@ export default function WebsiteSettings() {
const [showCode, setShowCode] = useState();
const [saved, setSaved] = useState(0);
const Buttons = row => (
<>
<Button icon={<Code />} size="small" onClick={() => setShowCode(row)}>
<div>Get Code</div>
</Button>
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
<div>Edit</div>
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
<div>Delete</div>
</Button>
</>
);
const columns = [
{ key: 'name', label: 'Name', className: styles.col },
{ key: 'domain', label: 'Domain', className: styles.col },
{
key: 'action',
className: styles.buttons,
render: row => (
<>
<Button icon={<Code />} size="small" onClick={() => setShowCode(row)}>
<div>Get Code</div>
</Button>
<Button icon={<Pen />} size="small" onClick={() => setEditWebsite(row)}>
<div>Edit</div>
</Button>
<Button icon={<Trash />} size="small" onClick={() => setDeleteWebsite(row)}>
<div>Delete</div>
</Button>
</>
),
render: Buttons,
},
];
@ -58,7 +59,7 @@ export default function WebsiteSettings() {
}
async function loadData() {
setData(await get(`/api/website`));
setData(await get(`/api/websites`));
}
useEffect(() => {

View File

@ -2,8 +2,16 @@ import React, { useState } from 'react';
import { Formik, Form, Field } from 'formik';
import Router from 'next/router';
import { post } from 'lib/web';
import Button from '../common/Button';
import FormLayout, { FormButtons, FormError, FormMessage, FormRow } from '../layout/FormLayout';
import Button from 'components/common/Button';
import FormLayout, {
FormButtons,
FormError,
FormMessage,
FormRow,
} from 'components/layout/FormLayout';
import Icon from 'components/common/Icon';
import Logo from 'assets/logo.svg';
import styles from './LoginForm.module.css';
const validate = ({ username, password }) => {
const errors = {};
@ -32,7 +40,7 @@ export default function LoginForm() {
};
return (
<FormLayout>
<FormLayout className={styles.login}>
<Formik
initialValues={{
username: '',
@ -43,6 +51,7 @@ export default function LoginForm() {
>
{() => (
<Form>
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
<h1 className="center">umami</h1>
<FormRow>
<label htmlFor="username">Username</label>

View File

@ -0,0 +1,11 @@
.login {
display: flex;
flex-direction: column;
margin-top: 80px;
}
.icon {
display: flex;
justify-content: center;
margin: 0 auto;
}

View File

@ -111,7 +111,5 @@ export function getDateArray(data, startDate, endDate, unit) {
arr.push({ t, y });
}
console.log({ unit, arr });
return arr;
}

218
lib/db.js
View File

@ -1,6 +1,5 @@
import { PrismaClient } from '@prisma/client';
import chalk from 'chalk';
import { getMetricsQuery, getPageviewsQuery, getRankingsQuery } from 'lib/queries';
const options = {
log: [
@ -39,220 +38,3 @@ export async function runQuery(query) {
throw e;
});
}
export async function getWebsite({ website_id, website_uuid }) {
return runQuery(
prisma.website.findOne({
where: {
...(website_id && { website_id }),
...(website_uuid && { website_uuid }),
},
}),
);
}
export async function getWebsites(user_id) {
return runQuery(
prisma.website.findMany({
where: {
user_id,
},
orderBy: {
name: 'asc',
},
}),
);
}
export async function createWebsite(user_id, data) {
return runQuery(
prisma.website.create({
data: {
account: {
connect: {
user_id,
},
},
...data,
},
}),
);
}
export async function updateWebsite(website_id, data) {
return runQuery(
prisma.website.update({
where: {
website_id,
},
data,
}),
);
}
export async function deleteWebsite(website_id) {
return runQuery(
/* Prisma bug, does not cascade on non-nullable foreign keys
prisma.website.delete({
where: {
website_id,
},
}),
*/
prisma.queryRaw(`delete from website where website_id=$1`, website_id),
);
}
export async function createSession(website_id, data) {
return runQuery(
prisma.session.create({
data: {
website: {
connect: {
website_id,
},
},
...data,
},
select: {
session_id: true,
},
}),
);
}
export async function getSession({ session_id, session_uuid }) {
return runQuery(
prisma.session.findOne({
where: {
session_id,
session_uuid,
},
}),
);
}
export async function savePageView(website_id, session_id, url, referrer) {
return runQuery(
prisma.pageview.create({
data: {
website: {
connect: {
website_id,
},
},
session: {
connect: {
session_id,
},
},
url,
referrer,
},
}),
);
}
export async function saveEvent(website_id, session_id, url, event_type, event_value) {
return runQuery(
prisma.event.create({
data: {
website: {
connect: {
website_id,
},
},
session: {
connect: {
session_id,
},
},
url,
event_type,
event_value,
},
}),
);
}
export async function getAccounts() {
return runQuery(prisma.account.findMany());
}
export async function getAccount({ user_id, username }) {
return runQuery(
prisma.account.findOne({
where: {
username,
user_id,
},
}),
);
}
export async function updateAccount(user_id, data) {
return runQuery(
prisma.account.update({
where: {
user_id,
},
data,
}),
);
}
export async function deleteAccount(user_id) {
return runQuery(
/* Prisma bug, does not cascade on non-nullable foreign keys
prisma.account.delete({
where: {
user_id,
},
}),
*/
prisma.queryRaw(`delete from account where user_id=$1`, user_id),
);
}
export async function createAccount(data) {
return runQuery(
prisma.account.create({
data,
}),
);
}
export async function getPageviews(website_id, start_at, end_at) {
return runQuery(
prisma.pageview.findMany({
where: {
website_id,
created_at: {
gte: start_at,
lte: end_at,
},
},
}),
);
}
export async function getRankings(website_id, start_at, end_at, type, table) {
return getRankingsQuery(prisma, { website_id, start_at, end_at, type, table });
}
export async function getPageviewData(
website_id,
start_at,
end_at,
timezone = 'utc',
unit = 'day',
count = '*',
) {
return runQuery(
getPageviewsQuery(prisma, { website_id, start_at, end_at, timezone, unit, count }),
);
}
export async function getMetrics(website_id, start_at, end_at) {
return getMetricsQuery(prisma, { website_id, start_at, end_at });
}

View File

@ -1,4 +1,5 @@
import moment from 'moment-timezone';
import prisma, { runQuery } from 'lib/db';
const POSTGRESQL = 'postgresql';
const MYSQL = 'mysql';
@ -7,7 +8,216 @@ export function getDatabase() {
return process.env.DATABASE_URL.split(':')[0];
}
export function getMetricsQuery(prisma, { website_id, start_at, end_at }) {
export async function getWebsiteById(website_id) {
return runQuery(
prisma.website.findOne({
where: {
website_id,
},
}),
);
}
export async function getWebsiteByUuid(website_uuid) {
return runQuery(
prisma.website.findOne({
where: {
website_uuid,
},
}),
);
}
export async function getUserWebsites(user_id) {
return runQuery(
prisma.website.findMany({
where: {
user_id,
},
orderBy: {
name: 'asc',
},
}),
);
}
export async function createWebsite(user_id, data) {
return runQuery(
prisma.website.create({
data: {
account: {
connect: {
user_id,
},
},
...data,
},
}),
);
}
export async function updateWebsite(website_id, data) {
return runQuery(
prisma.website.update({
where: {
website_id,
},
data,
}),
);
}
export async function deleteWebsite(website_id) {
return runQuery(
/* Prisma bug, does not cascade on non-nullable foreign keys
prisma.website.delete({
where: {
website_id,
},
}),
*/
prisma.$queryRaw`delete from website where website_id=${website_id}`,
);
}
export async function createSession(website_id, data) {
return runQuery(
prisma.session.create({
data: {
website: {
connect: {
website_id,
},
},
...data,
},
select: {
session_id: true,
},
}),
);
}
export async function getSessionById(session_id) {
return runQuery(
prisma.session.findOne({
where: {
session_id,
},
}),
);
}
export async function getSessionByUuid(session_uuid) {
return runQuery(
prisma.session.findOne({
where: {
session_uuid,
},
}),
);
}
export async function savePageView(website_id, session_id, url, referrer) {
return runQuery(
prisma.pageview.create({
data: {
website: {
connect: {
website_id,
},
},
session: {
connect: {
session_id,
},
},
url,
referrer,
},
}),
);
}
export async function saveEvent(website_id, session_id, url, event_type, event_value) {
return runQuery(
prisma.event.create({
data: {
website: {
connect: {
website_id,
},
},
session: {
connect: {
session_id,
},
},
url,
event_type,
event_value,
},
}),
);
}
export async function getAccounts() {
return runQuery(prisma.account.findMany());
}
export async function getAccountById(user_id) {
return runQuery(
prisma.account.findOne({
where: {
user_id,
},
}),
);
}
export async function getAccountByUsername(username) {
return runQuery(
prisma.account.findOne({
where: {
username,
},
}),
);
}
export async function updateAccount(user_id, data) {
return runQuery(
prisma.account.update({
where: {
user_id,
},
data,
}),
);
}
export async function deleteAccount(user_id) {
return runQuery(
/* Prisma bug, does not cascade on non-nullable foreign keys
prisma.account.delete({
where: {
user_id,
},
}),
*/
prisma.$queryRaw`delete from account where user_id=${user_id}`,
);
}
export async function createAccount(data) {
return runQuery(
prisma.account.create({
data,
}),
);
}
export function getMetrics(website_id, start_at, end_at) {
const db = getDatabase();
if (db === POSTGRESQL) {
@ -61,7 +271,14 @@ export function getMetricsQuery(prisma, { website_id, start_at, end_at }) {
return Promise.resolve({});
}
export function getPageviewsQuery(prisma, { website_id, start_at, end_at, unit, timezone, count }) {
export function getPageviews(
website_id,
start_at,
end_at,
timezone = 'utc',
unit = 'day',
count = '*',
) {
const db = getDatabase();
if (db === POSTGRESQL) {
@ -102,7 +319,7 @@ export function getPageviewsQuery(prisma, { website_id, start_at, end_at, unit,
return Promise.resolve([]);
}
export function getRankingsQuery(prisma, { website_id, start_at, end_at, type, table }) {
export function getRankings(website_id, start_at, end_at, type, table) {
const db = getDatabase();
if (db === POSTGRESQL) {

View File

@ -1,4 +1,4 @@
import { getWebsite, getSession, createSession } from 'lib/db';
import { getWebsiteByUuid, getSessionByUuid, createSession } from 'lib/queries';
import { getClientInfo } from 'lib/request';
import { uuid, isValidId, parseToken } from 'lib/crypto';
@ -19,7 +19,7 @@ export async function verifySession(req) {
if (!token || token.website_uuid !== website_uuid) {
const { userAgent, browser, os, ip, country, device } = await getClientInfo(req, payload);
const website = await getWebsite({ website_uuid });
const website = await getWebsiteByUuid(website_uuid);
if (!website) {
throw new Error(`Website not found: ${website_uuid}`);
@ -28,7 +28,7 @@ export async function verifySession(req) {
const { website_id } = website;
const session_uuid = uuid(website_id, hostname, ip, userAgent, os);
let session = await getSession({ session_uuid });
let session = await getSessionByUuid(session_uuid);
if (!session) {
session = await createSession(website_id, {

View File

@ -1,6 +1,6 @@
import { getAccounts, getAccount, updateAccount, createAccount } from 'lib/db';
import { getAccountById, getAccountByUsername, updateAccount, createAccount } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { hashPassword, uuid } from 'lib/crypto';
import { hashPassword } from 'lib/crypto';
import { ok, unauthorized, methodNotAllowed, badRequest } from 'lib/response';
export default async (req, res) => {
@ -8,24 +8,18 @@ export default async (req, res) => {
const { user_id: current_user_id, is_admin: current_user_is_admin } = req.auth;
if (req.method === 'GET') {
if (current_user_is_admin) {
const accounts = await getAccounts();
return ok(res, accounts);
}
return unauthorized(res);
}
if (req.method === 'POST') {
const { user_id, username, password, is_admin } = req.body;
if (user_id) {
const account = await getAccount({ user_id });
const account = await getAccountById(user_id);
if (account.user_id === current_user_id || current_user_is_admin) {
const data = { password: password ? await hashPassword(password) : undefined };
const data = {};
if (password) {
data.password = await hashPassword(password);
}
// Only admin can change these fields
if (current_user_is_admin) {
@ -37,7 +31,7 @@ export default async (req, res) => {
}
if (data.username && account.username !== data.username) {
const accountByUsername = await getAccount({ username });
const accountByUsername = await getAccountByUsername(username);
if (accountByUsername) {
return badRequest(res, 'Account already exists');
@ -51,7 +45,7 @@ export default async (req, res) => {
return unauthorized(res);
} else {
const accountByUsername = await getAccount({ username });
const accountByUsername = await getAccountByUsername(username);
if (accountByUsername) {
return badRequest(res, 'Account already exists');

View File

@ -1,4 +1,4 @@
import { getAccount, deleteAccount } from 'lib/db';
import { getAccountById, deleteAccount } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
@ -11,7 +11,7 @@ export default async (req, res) => {
if (req.method === 'GET') {
if (is_admin) {
const account = await getAccount({ user_id });
const account = await getAccountById(user_id);
return ok(res, account);
}

View File

@ -1,4 +1,4 @@
import { getAccount, updateAccount } from 'lib/db';
import { getAccountById, updateAccount } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { badRequest, methodNotAllowed, ok } from 'lib/response';
import { checkPassword, hashPassword } from 'lib/crypto';
@ -10,7 +10,7 @@ export default async (req, res) => {
const { current_password, new_password } = req.body;
if (req.method === 'POST') {
const account = await getAccount({ user_id });
const account = await getAccountById(user_id);
const valid = await checkPassword(current_password, account.password);
if (!valid) {

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

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

View File

@ -1,13 +1,13 @@
import { serialize } from 'cookie';
import { checkPassword, createSecureToken } from 'lib/crypto';
import { getAccount } from 'lib/db';
import { getAccountByUsername } from 'lib/queries';
import { AUTH_COOKIE_NAME } from 'lib/constants';
import { ok, unauthorized } from 'lib/response';
export default async (req, res) => {
const { username, password } = req.body;
const account = await getAccount({ username });
const account = await getAccountByUsername(username);
if (account && (await checkPassword(password, account.password))) {
const { user_id, username, is_admin } = account;

View File

@ -1,4 +1,4 @@
import { savePageView, saveEvent } from 'lib/db';
import { savePageView, saveEvent } from 'lib/queries';
import { useCors, useSession } from 'lib/middleware';
import { createToken } from 'lib/crypto';
import { ok, badRequest } from 'lib/response';

View File

@ -1,4 +1,4 @@
import { getWebsites, updateWebsite, createWebsite, getWebsite } from 'lib/db';
import { updateWebsite, createWebsite, getWebsiteById } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { uuid } from 'lib/crypto';
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
@ -9,17 +9,11 @@ export default async (req, res) => {
const { user_id, is_admin } = req.auth;
const { website_id } = req.body;
if (req.method === 'GET') {
const websites = await getWebsites(user_id);
return ok(res, websites);
}
if (req.method === 'POST') {
const { name, domain } = req.body;
if (website_id) {
const website = getWebsite(website_id);
const website = getWebsiteById(website_id);
if (website.user_id === user_id || is_admin) {
await updateWebsite(website_id, { name, domain });

View File

@ -1,4 +1,4 @@
import { deleteWebsite, getWebsite } from 'lib/db';
import { deleteWebsite, getWebsiteById } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
@ -10,13 +10,13 @@ export default async (req, res) => {
const website_id = +id;
if (req.method === 'GET') {
const website = await getWebsite({ website_id });
const website = await getWebsiteById(website_id);
return ok(res, website);
}
if (req.method === 'DELETE') {
const website = await getWebsite({ website_id });
const website = await getWebsiteById(website_id);
if (website.user_id === user_id || is_admin) {
await deleteWebsite(website_id);

View File

@ -1,4 +1,4 @@
import { getMetrics } from 'lib/db';
import { getMetrics } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { ok } from 'lib/response';

View File

@ -1,5 +1,5 @@
import moment from 'moment-timezone';
import { getPageviewData } from 'lib/db';
import { getPageviews } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { ok, badRequest } from 'lib/response';
@ -18,8 +18,8 @@ export default async (req, res) => {
const end = new Date(+end_at);
const [pageviews, uniques] = await Promise.all([
getPageviewData(+id, start, end, tz, unit, '*'),
getPageviewData(+id, start, end, tz, unit, 'distinct session_id'),
getPageviews(+id, start, end, tz, unit, '*'),
getPageviews(+id, start, end, tz, unit, 'distinct session_id'),
]);
return ok(res, { pageviews, uniques });

View File

@ -1,4 +1,4 @@
import { getRankings } from 'lib/db';
import { getRankings } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { ok, badRequest } from 'lib/response';

17
pages/api/websites.js Normal file
View File

@ -0,0 +1,17 @@
import { getUserWebsites } from 'lib/queries';
import { useAuth } from 'lib/middleware';
import { ok, methodNotAllowed } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
const { user_id } = req.auth;
if (req.method === 'GET') {
const websites = await getUserWebsites(user_id);
return ok(res, websites);
}
return methodNotAllowed(res);
};

View File

@ -1,13 +1,10 @@
import React from 'react';
import Layout from 'components/layout/Layout';
import LoginForm from 'components/forms/LoginForm';
import Icon from 'components/common/Icon';
import Logo from 'assets/logo.svg';
export default function LoginPage() {
return (
<Layout title="login" header={false} footer={false} center middle>
<Icon icon={<Logo />} size="xlarge" />
<Layout title="login" header={false} footer={false} center>
<LoginForm />
</Layout>
);