mirror of
https://github.com/kremalicious/umami.git
synced 2024-06-28 16:57:52 +02:00
Refactor database queries.
This commit is contained in:
parent
a248f35db2
commit
f4ca353b5c
|
@ -5,7 +5,6 @@ import Button from 'components/common/Button';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import Table from 'components/common/Table';
|
import Table from 'components/common/Table';
|
||||||
import Modal from 'components/common/Modal';
|
import Modal from 'components/common/Modal';
|
||||||
import WebsiteEditForm from 'components/forms/WebsiteEditForm';
|
|
||||||
import AccountEditForm from 'components/forms/AccountEditForm';
|
import AccountEditForm from 'components/forms/AccountEditForm';
|
||||||
import Pen from 'assets/pen.svg';
|
import Pen from 'assets/pen.svg';
|
||||||
import Plus from 'assets/plus.svg';
|
import Plus from 'assets/plus.svg';
|
||||||
|
@ -16,33 +15,36 @@ import styles from './AccountSettings.module.css';
|
||||||
import DeleteForm from './forms/DeleteForm';
|
import DeleteForm from './forms/DeleteForm';
|
||||||
|
|
||||||
export default function AccountSettings() {
|
export default function AccountSettings() {
|
||||||
const user = useSelector(state => state.user);
|
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
const [addAccount, setAddAccount] = useState();
|
const [addAccount, setAddAccount] = useState();
|
||||||
const [editAccount, setEditAccount] = useState();
|
const [editAccount, setEditAccount] = useState();
|
||||||
const [deleteAccount, setDeleteAccount] = useState();
|
const [deleteAccount, setDeleteAccount] = useState();
|
||||||
const [saved, setSaved] = useState(0);
|
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 = [
|
const columns = [
|
||||||
{ key: 'username', label: 'Username' },
|
{ key: 'username', label: 'Username' },
|
||||||
{
|
{
|
||||||
key: 'is_admin',
|
key: 'is_admin',
|
||||||
label: 'Administrator',
|
label: 'Administrator',
|
||||||
render: ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null),
|
render: Checkmark,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
className: styles.buttons,
|
className: styles.buttons,
|
||||||
render: row =>
|
render: Buttons,
|
||||||
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,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -58,7 +60,7 @@ export default function AccountSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setData(await get(`/api/account`));
|
setData(await get(`/api/accounts`));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
import { useSelector } from 'react-redux';
|
||||||
import PageHeader from './layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Button from './common/Button';
|
import Button from 'components/common/Button';
|
||||||
import ChangePasswordForm from './forms/ChangePasswordForm';
|
import ChangePasswordForm from './forms/ChangePasswordForm';
|
||||||
import Modal from './common/Modal';
|
import Modal from 'components/common/Modal';
|
||||||
|
|
||||||
export default function ProfileSettings() {
|
export default function ProfileSettings() {
|
||||||
const user = useSelector(state => state.user);
|
const user = useSelector(state => state.user);
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import WebsiteChart from './charts/WebsiteChart';
|
import WebsiteChart from 'components/charts/WebsiteChart';
|
||||||
import RankingsChart from './charts/RankingsChart';
|
import RankingsChart from 'components/charts/RankingsChart';
|
||||||
import WorldMap from './common/WorldMap';
|
import WorldMap from 'components/common/WorldMap';
|
||||||
import Page from './layout/Page';
|
import Page from 'components/layout/Page';
|
||||||
import PageHeader from './layout/PageHeader';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import MenuLayout from './layout/MenuLayout';
|
import MenuLayout from 'components/layout/MenuLayout';
|
||||||
import Button from './common/Button';
|
import Button from 'components/common/Button';
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'lib/web';
|
||||||
import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters';
|
import { browserFilter, urlFilter, refFilter, deviceFilter, countryFilter } from 'lib/filters';
|
||||||
|
|
|
@ -1,22 +1,21 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get } from 'lib/web';
|
import Link from 'components/common/Link';
|
||||||
import Link from './common/Link';
|
import WebsiteChart from 'components/charts/WebsiteChart';
|
||||||
import WebsiteChart from './charts/WebsiteChart';
|
import Page from 'components/layout/Page';
|
||||||
import Page from './layout/Page';
|
import Button from 'components/common/Button';
|
||||||
import Icon from './common/Icon';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
import Button from './common/Button';
|
import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import PageHeader from './layout/PageHeader';
|
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
|
import { get } from 'lib/web';
|
||||||
import styles from './WebsiteList.module.css';
|
import styles from './WebsiteList.module.css';
|
||||||
import EmptyPlaceholder from './common/EmptyPlaceholder';
|
|
||||||
|
|
||||||
export default function WebsiteList() {
|
export default function WebsiteList() {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setData(await get(`/api/website`));
|
setData(await get(`/api/websites`));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,19 +1,18 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import Table from './common/Table';
|
import Table from 'components/common/Table';
|
||||||
import Button from './common/Button';
|
import Button from 'components/common/Button';
|
||||||
import PageHeader from './layout/PageHeader';
|
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 Pen from 'assets/pen.svg';
|
||||||
import Trash from 'assets/trash.svg';
|
import Trash from 'assets/trash.svg';
|
||||||
import Plus from 'assets/plus.svg';
|
import Plus from 'assets/plus.svg';
|
||||||
import Code from 'assets/code.svg';
|
import Code from 'assets/code.svg';
|
||||||
import { get } from 'lib/web';
|
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 styles from './WebsiteSettings.module.css';
|
||||||
import EmptyPlaceholder from './common/EmptyPlaceholder';
|
|
||||||
import Arrow from '../assets/arrow-right.svg';
|
|
||||||
|
|
||||||
export default function WebsiteSettings() {
|
export default function WebsiteSettings() {
|
||||||
const [data, setData] = useState();
|
const [data, setData] = useState();
|
||||||
|
@ -23,25 +22,27 @@ export default function WebsiteSettings() {
|
||||||
const [showCode, setShowCode] = useState();
|
const [showCode, setShowCode] = useState();
|
||||||
const [saved, setSaved] = useState(0);
|
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 = [
|
const columns = [
|
||||||
{ key: 'name', label: 'Name', className: styles.col },
|
{ key: 'name', label: 'Name', className: styles.col },
|
||||||
{ key: 'domain', label: 'Domain', className: styles.col },
|
{ key: 'domain', label: 'Domain', className: styles.col },
|
||||||
{
|
{
|
||||||
key: 'action',
|
key: 'action',
|
||||||
className: styles.buttons,
|
className: styles.buttons,
|
||||||
render: row => (
|
render: Buttons,
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -58,7 +59,7 @@ export default function WebsiteSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
setData(await get(`/api/website`));
|
setData(await get(`/api/websites`));
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -2,8 +2,16 @@ import React, { useState } from 'react';
|
||||||
import { Formik, Form, Field } from 'formik';
|
import { Formik, Form, Field } from 'formik';
|
||||||
import Router from 'next/router';
|
import Router from 'next/router';
|
||||||
import { post } from 'lib/web';
|
import { post } from 'lib/web';
|
||||||
import Button from '../common/Button';
|
import Button from 'components/common/Button';
|
||||||
import FormLayout, { FormButtons, FormError, FormMessage, FormRow } from '../layout/FormLayout';
|
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 validate = ({ username, password }) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
@ -32,7 +40,7 @@ export default function LoginForm() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormLayout>
|
<FormLayout className={styles.login}>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
username: '',
|
username: '',
|
||||||
|
@ -43,6 +51,7 @@ export default function LoginForm() {
|
||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<Form>
|
<Form>
|
||||||
|
<Icon icon={<Logo />} size="xlarge" className={styles.icon} />
|
||||||
<h1 className="center">umami</h1>
|
<h1 className="center">umami</h1>
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<label htmlFor="username">Username</label>
|
<label htmlFor="username">Username</label>
|
||||||
|
|
11
components/forms/LoginForm.module.css
Normal file
11
components/forms/LoginForm.module.css
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
.login {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-top: 80px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
|
@ -111,7 +111,5 @@ export function getDateArray(data, startDate, endDate, unit) {
|
||||||
arr.push({ t, y });
|
arr.push({ t, y });
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log({ unit, arr });
|
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
}
|
}
|
||||||
|
|
218
lib/db.js
218
lib/db.js
|
@ -1,6 +1,5 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
import { PrismaClient } from '@prisma/client';
|
||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
import { getMetricsQuery, getPageviewsQuery, getRankingsQuery } from 'lib/queries';
|
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
log: [
|
log: [
|
||||||
|
@ -39,220 +38,3 @@ export async function runQuery(query) {
|
||||||
throw e;
|
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 });
|
|
||||||
}
|
|
||||||
|
|
223
lib/queries.js
223
lib/queries.js
|
@ -1,4 +1,5 @@
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
|
import prisma, { runQuery } from 'lib/db';
|
||||||
|
|
||||||
const POSTGRESQL = 'postgresql';
|
const POSTGRESQL = 'postgresql';
|
||||||
const MYSQL = 'mysql';
|
const MYSQL = 'mysql';
|
||||||
|
@ -7,7 +8,216 @@ export function getDatabase() {
|
||||||
return process.env.DATABASE_URL.split(':')[0];
|
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();
|
const db = getDatabase();
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
if (db === POSTGRESQL) {
|
||||||
|
@ -61,7 +271,14 @@ export function getMetricsQuery(prisma, { website_id, start_at, end_at }) {
|
||||||
return Promise.resolve({});
|
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();
|
const db = getDatabase();
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
if (db === POSTGRESQL) {
|
||||||
|
@ -102,7 +319,7 @@ export function getPageviewsQuery(prisma, { website_id, start_at, end_at, unit,
|
||||||
return Promise.resolve([]);
|
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();
|
const db = getDatabase();
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
if (db === POSTGRESQL) {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getWebsite, getSession, createSession } from 'lib/db';
|
import { getWebsiteByUuid, getSessionByUuid, createSession } from 'lib/queries';
|
||||||
import { getClientInfo } from 'lib/request';
|
import { getClientInfo } from 'lib/request';
|
||||||
import { uuid, isValidId, parseToken } from 'lib/crypto';
|
import { uuid, isValidId, parseToken } from 'lib/crypto';
|
||||||
|
|
||||||
|
@ -19,7 +19,7 @@ export async function verifySession(req) {
|
||||||
if (!token || token.website_uuid !== website_uuid) {
|
if (!token || token.website_uuid !== website_uuid) {
|
||||||
const { userAgent, browser, os, ip, country, device } = await getClientInfo(req, payload);
|
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) {
|
if (!website) {
|
||||||
throw new Error(`Website not found: ${website_uuid}`);
|
throw new Error(`Website not found: ${website_uuid}`);
|
||||||
|
@ -28,7 +28,7 @@ export async function verifySession(req) {
|
||||||
const { website_id } = website;
|
const { website_id } = website;
|
||||||
const session_uuid = uuid(website_id, hostname, ip, userAgent, os);
|
const session_uuid = uuid(website_id, hostname, ip, userAgent, os);
|
||||||
|
|
||||||
let session = await getSession({ session_uuid });
|
let session = await getSessionByUuid(session_uuid);
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
session = await createSession(website_id, {
|
session = await createSession(website_id, {
|
||||||
|
|
|
@ -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 { useAuth } from 'lib/middleware';
|
||||||
import { hashPassword, uuid } from 'lib/crypto';
|
import { hashPassword } from 'lib/crypto';
|
||||||
import { ok, unauthorized, methodNotAllowed, badRequest } from 'lib/response';
|
import { ok, unauthorized, methodNotAllowed, badRequest } from 'lib/response';
|
||||||
|
|
||||||
export default async (req, res) => {
|
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;
|
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') {
|
if (req.method === 'POST') {
|
||||||
const { user_id, username, password, is_admin } = req.body;
|
const { user_id, username, password, is_admin } = req.body;
|
||||||
|
|
||||||
if (user_id) {
|
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) {
|
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
|
// Only admin can change these fields
|
||||||
if (current_user_is_admin) {
|
if (current_user_is_admin) {
|
||||||
|
@ -37,7 +31,7 @@ export default async (req, res) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.username && account.username !== data.username) {
|
if (data.username && account.username !== data.username) {
|
||||||
const accountByUsername = await getAccount({ username });
|
const accountByUsername = await getAccountByUsername(username);
|
||||||
|
|
||||||
if (accountByUsername) {
|
if (accountByUsername) {
|
||||||
return badRequest(res, 'Account already exists');
|
return badRequest(res, 'Account already exists');
|
||||||
|
@ -51,7 +45,7 @@ export default async (req, res) => {
|
||||||
|
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
} else {
|
} else {
|
||||||
const accountByUsername = await getAccount({ username });
|
const accountByUsername = await getAccountByUsername(username);
|
||||||
|
|
||||||
if (accountByUsername) {
|
if (accountByUsername) {
|
||||||
return badRequest(res, 'Account already exists');
|
return badRequest(res, 'Account already exists');
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getAccount, deleteAccount } from 'lib/db';
|
import { getAccountById, deleteAccount } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ export default async (req, res) => {
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
if (is_admin) {
|
if (is_admin) {
|
||||||
const account = await getAccount({ user_id });
|
const account = await getAccountById(user_id);
|
||||||
|
|
||||||
return ok(res, account);
|
return ok(res, account);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getAccount, updateAccount } from 'lib/db';
|
import { getAccountById, updateAccount } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { badRequest, methodNotAllowed, ok } from 'lib/response';
|
import { badRequest, methodNotAllowed, ok } from 'lib/response';
|
||||||
import { checkPassword, hashPassword } from 'lib/crypto';
|
import { checkPassword, hashPassword } from 'lib/crypto';
|
||||||
|
@ -10,7 +10,7 @@ export default async (req, res) => {
|
||||||
const { current_password, new_password } = req.body;
|
const { current_password, new_password } = req.body;
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const account = await getAccount({ user_id });
|
const account = await getAccountById(user_id);
|
||||||
const valid = await checkPassword(current_password, account.password);
|
const valid = await checkPassword(current_password, account.password);
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
|
21
pages/api/accounts.js
Normal file
21
pages/api/accounts.js
Normal 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);
|
||||||
|
};
|
|
@ -1,13 +1,13 @@
|
||||||
import { serialize } from 'cookie';
|
import { serialize } from 'cookie';
|
||||||
import { checkPassword, createSecureToken } from 'lib/crypto';
|
import { checkPassword, createSecureToken } from 'lib/crypto';
|
||||||
import { getAccount } from 'lib/db';
|
import { getAccountByUsername } from 'lib/queries';
|
||||||
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
||||||
import { ok, unauthorized } from 'lib/response';
|
import { ok, unauthorized } from 'lib/response';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
|
||||||
const account = await getAccount({ username });
|
const account = await getAccountByUsername(username);
|
||||||
|
|
||||||
if (account && (await checkPassword(password, account.password))) {
|
if (account && (await checkPassword(password, account.password))) {
|
||||||
const { user_id, username, is_admin } = account;
|
const { user_id, username, is_admin } = account;
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { savePageView, saveEvent } from 'lib/db';
|
import { savePageView, saveEvent } from 'lib/queries';
|
||||||
import { useCors, useSession } from 'lib/middleware';
|
import { useCors, useSession } from 'lib/middleware';
|
||||||
import { createToken } from 'lib/crypto';
|
import { createToken } from 'lib/crypto';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest } from 'lib/response';
|
||||||
|
|
|
@ -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 { useAuth } from 'lib/middleware';
|
||||||
import { uuid } from 'lib/crypto';
|
import { uuid } from 'lib/crypto';
|
||||||
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
|
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
|
||||||
|
@ -9,17 +9,11 @@ export default async (req, res) => {
|
||||||
const { user_id, is_admin } = req.auth;
|
const { user_id, is_admin } = req.auth;
|
||||||
const { website_id } = req.body;
|
const { website_id } = req.body;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
const websites = await getWebsites(user_id);
|
|
||||||
|
|
||||||
return ok(res, websites);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const { name, domain } = req.body;
|
const { name, domain } = req.body;
|
||||||
|
|
||||||
if (website_id) {
|
if (website_id) {
|
||||||
const website = getWebsite(website_id);
|
const website = getWebsiteById(website_id);
|
||||||
|
|
||||||
if (website.user_id === user_id || is_admin) {
|
if (website.user_id === user_id || is_admin) {
|
||||||
await updateWebsite(website_id, { name, domain });
|
await updateWebsite(website_id, { name, domain });
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { deleteWebsite, getWebsite } from 'lib/db';
|
import { deleteWebsite, getWebsiteById } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
||||||
|
|
||||||
|
@ -10,13 +10,13 @@ export default async (req, res) => {
|
||||||
const website_id = +id;
|
const website_id = +id;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const website = await getWebsite({ website_id });
|
const website = await getWebsiteById(website_id);
|
||||||
|
|
||||||
return ok(res, website);
|
return ok(res, website);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'DELETE') {
|
if (req.method === 'DELETE') {
|
||||||
const website = await getWebsite({ website_id });
|
const website = await getWebsiteById(website_id);
|
||||||
|
|
||||||
if (website.user_id === user_id || is_admin) {
|
if (website.user_id === user_id || is_admin) {
|
||||||
await deleteWebsite(website_id);
|
await deleteWebsite(website_id);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getMetrics } from 'lib/db';
|
import { getMetrics } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok } from 'lib/response';
|
import { ok } from 'lib/response';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { getPageviewData } from 'lib/db';
|
import { getPageviews } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest } from 'lib/response';
|
||||||
|
|
||||||
|
@ -18,8 +18,8 @@ export default async (req, res) => {
|
||||||
const end = new Date(+end_at);
|
const end = new Date(+end_at);
|
||||||
|
|
||||||
const [pageviews, uniques] = await Promise.all([
|
const [pageviews, uniques] = await Promise.all([
|
||||||
getPageviewData(+id, start, end, tz, unit, '*'),
|
getPageviews(+id, start, end, tz, unit, '*'),
|
||||||
getPageviewData(+id, start, end, tz, unit, 'distinct session_id'),
|
getPageviews(+id, start, end, tz, unit, 'distinct session_id'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return ok(res, { pageviews, uniques });
|
return ok(res, { pageviews, uniques });
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { getRankings } from 'lib/db';
|
import { getRankings } from 'lib/queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, badRequest } from 'lib/response';
|
import { ok, badRequest } from 'lib/response';
|
||||||
|
|
||||||
|
|
17
pages/api/websites.js
Normal file
17
pages/api/websites.js
Normal 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);
|
||||||
|
};
|
|
@ -1,13 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Layout from 'components/layout/Layout';
|
import Layout from 'components/layout/Layout';
|
||||||
import LoginForm from 'components/forms/LoginForm';
|
import LoginForm from 'components/forms/LoginForm';
|
||||||
import Icon from 'components/common/Icon';
|
|
||||||
import Logo from 'assets/logo.svg';
|
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
<Layout title="login" header={false} footer={false} center middle>
|
<Layout title="login" header={false} footer={false} center>
|
||||||
<Icon icon={<Logo />} size="xlarge" />
|
|
||||||
<LoginForm />
|
<LoginForm />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|
Loading…
Reference in New Issue
Block a user