mirror of
https://github.com/kremalicious/umami.git
synced 2024-12-24 02:06:19 +01:00
Handle website delete. Added response helper functions.
This commit is contained in:
parent
0a411a9ad6
commit
c4b75e4aec
@ -8,13 +8,15 @@ import Trash from 'assets/trash.svg';
|
||||
import Plus from 'assets/plus.svg';
|
||||
import { get } from 'lib/web';
|
||||
import Modal from './common/Modal';
|
||||
import WebsiteForm from './forms/WebsiteForm';
|
||||
import WebsiteEditForm from './forms/WebsiteEditForm';
|
||||
import styles from './Settings.module.css';
|
||||
import WebsiteDeleteForm from './forms/WebsiteDeleteForm';
|
||||
|
||||
export default function Settings() {
|
||||
const [data, setData] = useState();
|
||||
const [edit, setEdit] = useState();
|
||||
const [del, setDelete] = useState();
|
||||
const [add, setAdd] = useState();
|
||||
const [saved, setSaved] = useState(0);
|
||||
|
||||
const columns = [
|
||||
@ -44,6 +46,7 @@ export default function Settings() {
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setAdd(null);
|
||||
setEdit(null);
|
||||
setDelete(null);
|
||||
}
|
||||
@ -64,14 +67,28 @@ export default function Settings() {
|
||||
<Page>
|
||||
<PageHeader>
|
||||
<div>Websites</div>
|
||||
<Button icon={<Plus />} size="S">
|
||||
<Button icon={<Plus />} size="S" onClick={() => setAdd(true)}>
|
||||
<div>Add website</div>
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} />
|
||||
{edit && (
|
||||
<Modal title="Edit website">
|
||||
<WebsiteForm initialValues={edit} onSave={handleSave} onClose={handleClose} />
|
||||
<WebsiteEditForm initialValues={edit} onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{add && (
|
||||
<Modal title="Add website">
|
||||
<WebsiteEditForm
|
||||
initialValues={{ name: '', domain: '' }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{del && (
|
||||
<Modal title="Delete website">
|
||||
<WebsiteDeleteForm initialValues={del} onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
</Page>
|
||||
|
@ -36,7 +36,7 @@ export default function DropDown({
|
||||
<div ref={ref} className={classNames(styles.dropdown, className)} onClick={handleShowMenu}>
|
||||
<div className={styles.value}>
|
||||
{options.find(e => e.value === value)?.label}
|
||||
<Icon icon={<Chevron />} size="S" className={styles.icon} />
|
||||
<Icon icon={<Chevron />} size="S" />
|
||||
</div>
|
||||
{showMenu && <Menu className={menuClassName} options={options} onSelect={handleSelect} />}
|
||||
</div>
|
||||
|
@ -5,18 +5,12 @@
|
||||
}
|
||||
|
||||
.value {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
padding: 4px 32px 4px 16px;
|
||||
padding: 4px 16px;
|
||||
border: 1px solid var(--gray500);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 12px;
|
||||
margin: auto;
|
||||
}
|
||||
|
@ -31,7 +31,6 @@
|
||||
border: 1px solid var(--gray300);
|
||||
padding: 30px;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.header {
|
||||
|
@ -56,7 +56,7 @@ export default function LoginForm() {
|
||||
<FormError name="password" />
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button className={styles.button} type="submit">
|
||||
<Button type="submit" variant="action">
|
||||
Login
|
||||
</Button>
|
||||
</FormButtons>
|
||||
|
68
components/forms/WebsiteDeleteForm.js
Normal file
68
components/forms/WebsiteDeleteForm.js
Normal file
@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import { del } from 'lib/web';
|
||||
import Button from 'components/interface/Button';
|
||||
import FormLayout, {
|
||||
FormButtons,
|
||||
FormError,
|
||||
FormMessage,
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
|
||||
const validate = ({ confirmation }) => {
|
||||
const errors = {};
|
||||
|
||||
if (confirmation !== 'DELETE') {
|
||||
errors.confirmation = !confirmation ? 'Required' : 'Invalid';
|
||||
}
|
||||
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default function WebsiteDeleteForm({ initialValues, onSave, onClose }) {
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async ({ website_id }) => {
|
||||
const response = await del(`/api/website/${website_id}`);
|
||||
|
||||
if (response) {
|
||||
onSave();
|
||||
} else {
|
||||
setMessage('Something went wrong.');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
<Formik
|
||||
initialValues={{ confirmation: '', ...initialValues }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{() => (
|
||||
<Form>
|
||||
<div>
|
||||
Are your sure you want to delete <b>{initialValues.name}</b>?
|
||||
</div>
|
||||
<div>All associated data will be deleted as well.</div>
|
||||
<p>
|
||||
Type <b>DELETE</b> in the box below to confirm.
|
||||
</p>
|
||||
<FormRow>
|
||||
<label htmlFor="confirmation">Confirm</label>
|
||||
<Field name="confirmation" />
|
||||
<FormError name="confirmation" />
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit" variant="danger">
|
||||
Delete
|
||||
</Button>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
@ -1,6 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Formik, Form, Field } from 'formik';
|
||||
import Router from 'next/router';
|
||||
import { post } from 'lib/web';
|
||||
import Button from 'components/interface/Button';
|
||||
import FormLayout, {
|
||||
@ -23,7 +22,7 @@ const validate = ({ name, domain }) => {
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default function WebsiteForm({ initialValues, onSave, onClose }) {
|
||||
export default function WebsiteEditForm({ initialValues, onSave, onClose }) {
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
@ -52,7 +51,9 @@ export default function WebsiteForm({ initialValues, onSave, onClose }) {
|
||||
<FormError name="domain" />
|
||||
</FormRow>
|
||||
<FormButtons>
|
||||
<Button type="submit">Save</Button>
|
||||
<Button type="submit" variant="action">
|
||||
Save
|
||||
</Button>
|
||||
<Button onClick={onClose}>Cancel</Button>
|
||||
</FormButtons>
|
||||
<FormMessage>{message}</FormMessage>
|
@ -7,6 +7,7 @@ export default function Button({
|
||||
type = 'button',
|
||||
icon,
|
||||
size,
|
||||
variant,
|
||||
children,
|
||||
className,
|
||||
onClick = () => {},
|
||||
@ -17,6 +18,8 @@ export default function Button({
|
||||
className={classNames(styles.button, className, {
|
||||
[styles.small]: size === 'S',
|
||||
[styles.large]: size === 'L',
|
||||
[styles.action]: variant === 'action',
|
||||
[styles.danger]: variant === 'danger',
|
||||
})}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
@ -26,3 +26,21 @@
|
||||
.large {
|
||||
font-size: var(--font-size-large);
|
||||
}
|
||||
|
||||
.action {
|
||||
color: var(--gray50) !important;
|
||||
background: var(--gray900) !important;
|
||||
}
|
||||
|
||||
.action:hover {
|
||||
background: var(--gray800) !important;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: var(--gray50) !important;
|
||||
background: var(--red500) !important;
|
||||
}
|
||||
|
||||
.danger:hover {
|
||||
background: var(--red400) !important;
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import classNames from 'classnames';
|
||||
import { ErrorMessage } from 'formik';
|
||||
import styles from './FormLayout.module.css';
|
||||
@ -11,9 +12,19 @@ export const FormButtons = ({ className, children }) => (
|
||||
<div className={classNames(styles.buttons, className)}>{children}</div>
|
||||
);
|
||||
|
||||
export const FormError = ({ name }) => (
|
||||
<ErrorMessage name={name}>{msg => <div className={styles.error}>{msg}</div>}</ErrorMessage>
|
||||
);
|
||||
export const FormError = ({ name }) => {
|
||||
return <ErrorMessage name={name}>{msg => <ErrorTag msg={msg} />}</ErrorMessage>;
|
||||
};
|
||||
|
||||
const ErrorTag = ({ msg }) => {
|
||||
const props = useSpring({ opacity: 1, from: { opacity: 0 } });
|
||||
|
||||
return (
|
||||
<animated.div className={styles.error} style={props}>
|
||||
{msg}
|
||||
</animated.div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FormRow = ({ children }) => <div className={styles.row}>{children}</div>;
|
||||
|
||||
|
@ -20,11 +20,12 @@
|
||||
.buttons {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--gray50);
|
||||
background: var(--color-error);
|
||||
background: var(--red400);
|
||||
font-size: var(--font-size-small);
|
||||
position: absolute;
|
||||
display: flex;
|
||||
@ -47,7 +48,7 @@
|
||||
margin: auto;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: var(--color-error);
|
||||
background: var(--red400);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { parse } from 'cookie';
|
||||
import { verifySecureToken } from './crypto';
|
||||
import { parseSecureToken } from './crypto';
|
||||
import { AUTH_COOKIE_NAME } from './constants';
|
||||
|
||||
export async function verifyAuthToken(req) {
|
||||
const token = parse(req.headers.cookie || '')[AUTH_COOKIE_NAME];
|
||||
|
||||
return verifySecureToken(token);
|
||||
return parseSecureToken(token);
|
||||
}
|
||||
|
@ -1,9 +1,8 @@
|
||||
import crypto from 'crypto';
|
||||
import { v5 } from 'uuid';
|
||||
import { v4, v5, validate } from 'uuid';
|
||||
import bcrypt from 'bcrypt';
|
||||
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 hash(...args) {
|
||||
@ -15,11 +14,13 @@ export function secret() {
|
||||
}
|
||||
|
||||
export function uuid(...args) {
|
||||
if (!args.length) return v4();
|
||||
|
||||
return v5(args.join(''), v5(process.env.HASH_SALT, v5.DNS));
|
||||
}
|
||||
|
||||
export function isValidId(s) {
|
||||
return UUID_REGEX.test(s);
|
||||
return validate(s);
|
||||
}
|
||||
|
||||
export function checkPassword(password, hash) {
|
||||
@ -30,15 +31,24 @@ export async function createToken(payload) {
|
||||
return JWT.sign(payload, KEY);
|
||||
}
|
||||
|
||||
export async function verifyToken(token) {
|
||||
return JWT.verify(token, 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 verifySecureToken(token) {
|
||||
const result = await JWE.decrypt(token, KEY);
|
||||
return verifyToken(result.toString());
|
||||
export async function parseSecureToken(token) {
|
||||
try {
|
||||
const result = await JWE.decrypt(token, KEY);
|
||||
|
||||
return parseToken(result.toString());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
28
lib/db.js
28
lib/db.js
@ -63,6 +63,21 @@ export async function getWebsites(user_id) {
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
@ -74,6 +89,19 @@ export async function updateWebsite(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({
|
||||
|
@ -17,19 +17,23 @@ export function use(middleware) {
|
||||
export const useCors = use(cors());
|
||||
|
||||
export const useSession = use(async (req, res, next) => {
|
||||
try {
|
||||
req.session = await verifySession(req);
|
||||
} catch {
|
||||
const session = await verifySession(req);
|
||||
|
||||
if (!session) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
req.session = session;
|
||||
next();
|
||||
});
|
||||
|
||||
export const useAuth = use(async (req, res, next) => {
|
||||
try {
|
||||
req.auth = await verifyAuthToken(req);
|
||||
} catch {
|
||||
const token = await verifyAuthToken(req);
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
req.auth = token;
|
||||
next();
|
||||
});
|
||||
|
25
lib/response.js
Normal file
25
lib/response.js
Normal file
@ -0,0 +1,25 @@
|
||||
export function ok(res, data = {}) {
|
||||
return res.status(200).json(data);
|
||||
}
|
||||
|
||||
export function redirect(res, url) {
|
||||
res.setHeader('Location', url);
|
||||
|
||||
return res.status(303).end();
|
||||
}
|
||||
|
||||
export function badRequest(res) {
|
||||
return res.status(400).end();
|
||||
}
|
||||
|
||||
export function unauthorized(res) {
|
||||
return res.status(401).end();
|
||||
}
|
||||
|
||||
export function forbidden(res) {
|
||||
return res.status(403).end();
|
||||
}
|
||||
|
||||
export function methodNotAllowed(res) {
|
||||
res.status(405).end();
|
||||
}
|
@ -1,18 +1,14 @@
|
||||
import { getWebsite, getSession, createSession } from 'lib/db';
|
||||
import { getClientInfo } from 'lib/request';
|
||||
import { uuid, isValidId, verifyToken } from 'lib/crypto';
|
||||
import { uuid, isValidId, parseToken } from 'lib/crypto';
|
||||
|
||||
export async function verifySession(req) {
|
||||
const { payload } = req.body;
|
||||
const { website: website_uuid, hostname, screen, language, session } = payload;
|
||||
|
||||
if (!isValidId(website_uuid)) {
|
||||
throw new Error(`Invalid website: ${website_uuid}`);
|
||||
}
|
||||
const token = await parseToken(session);
|
||||
|
||||
try {
|
||||
return await verifyToken(session);
|
||||
} catch {
|
||||
if (!token || !isValidId(website_uuid) || token.website_uuid !== website_uuid) {
|
||||
const { userAgent, browser, os, ip, country, device } = await getClientInfo(req, payload);
|
||||
|
||||
if (website_uuid) {
|
||||
@ -50,4 +46,6 @@ export async function verifySession(req) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return token;
|
||||
}
|
||||
|
@ -26,9 +26,11 @@ function parseQuery(url, params = {}) {
|
||||
|
||||
export const get = (url, params) => apiRequest('get', parseQuery(url, params));
|
||||
|
||||
export const del = (url, params) => apiRequest('delete', parseQuery(url, params));
|
||||
|
||||
export const post = (url, params) => apiRequest('post', url, JSON.stringify(params));
|
||||
|
||||
export const del = (url, params) => apiRequest('del', parseQuery(url, params));
|
||||
export const put = (url, params) => apiRequest('put', url, JSON.stringify(params));
|
||||
|
||||
export const hook = (_this, method, callback) => {
|
||||
const orig = _this[method];
|
||||
|
@ -2,6 +2,7 @@ import { serialize } from 'cookie';
|
||||
import { checkPassword, createSecureToken } from 'lib/crypto';
|
||||
import { getAccount } from 'lib/db';
|
||||
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
||||
import { ok, unauthorized } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
@ -19,8 +20,8 @@ export default async (req, res) => {
|
||||
|
||||
res.setHeader('Set-Cookie', [cookie]);
|
||||
|
||||
return res.status(200).json({ token });
|
||||
return ok(res, { token });
|
||||
}
|
||||
|
||||
return res.status(401).end();
|
||||
return unauthorized(res);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { serialize } from 'cookie';
|
||||
import { AUTH_COOKIE_NAME } from 'lib/constants';
|
||||
import { redirect } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
const cookie = serialize(AUTH_COOKIE_NAME, '', {
|
||||
@ -8,9 +9,7 @@ export default async (req, res) => {
|
||||
maxAge: 0,
|
||||
});
|
||||
|
||||
res.statusCode = 303;
|
||||
res.setHeader('Set-Cookie', [cookie]);
|
||||
res.setHeader('Location', '/login');
|
||||
|
||||
return res.end();
|
||||
return redirect(res, '/login');
|
||||
};
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok, unauthorized } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.auth) {
|
||||
return res.status(200).json(req.auth);
|
||||
return ok(res, req.auth);
|
||||
}
|
||||
|
||||
return res.status(401).end();
|
||||
return unauthorized(res);
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { savePageView, saveEvent } from 'lib/db';
|
||||
import { useCors, useSession } from 'lib/middleware';
|
||||
import { createToken } from 'lib/crypto';
|
||||
import { ok, badRequest } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useCors(req, res);
|
||||
@ -10,21 +11,18 @@ export default async (req, res) => {
|
||||
const token = await createToken(session);
|
||||
const { website_id, session_id } = session;
|
||||
const { type, payload } = req.body;
|
||||
let ok = false;
|
||||
|
||||
if (type === 'pageview') {
|
||||
const { url, referrer } = payload;
|
||||
|
||||
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);
|
||||
|
||||
ok = true;
|
||||
} else {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
return res.status(200).json({ ok, session: token });
|
||||
return ok(res, { session: token });
|
||||
};
|
||||
|
@ -1,12 +1,13 @@
|
||||
import { verifySecureToken } from 'lib/crypto';
|
||||
import { parseSecureToken } from 'lib/crypto';
|
||||
import { ok, badRequest } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
const { token } = req.body;
|
||||
|
||||
try {
|
||||
const payload = await verifySecureToken(token);
|
||||
return res.status(200).json(payload);
|
||||
const payload = await parseSecureToken(token);
|
||||
return ok(res, payload);
|
||||
} catch {
|
||||
return res.status(400).end();
|
||||
return badRequest(res);
|
||||
}
|
||||
};
|
||||
|
@ -1,26 +1,40 @@
|
||||
import { getWebsites, updateWebsite } from 'lib/db';
|
||||
import { getWebsites, updateWebsite, createWebsite, getWebsite } from 'lib/db';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
const { user_id } = req.auth;
|
||||
const { user_id, is_admin } = req.auth;
|
||||
const { website_id } = req.body;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const websites = await getWebsites(user_id);
|
||||
|
||||
return res.status(200).json(websites);
|
||||
return ok(res, websites);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
if (website_id) {
|
||||
const { name, domain } = req.body;
|
||||
const website = await updateWebsite(website_id, { name, domain });
|
||||
const { name, domain } = req.body;
|
||||
|
||||
return res.status(200).json(website);
|
||||
if (website_id) {
|
||||
const website = getWebsite(website_id);
|
||||
|
||||
if (website.user_id === user_id || is_admin) {
|
||||
await updateWebsite(website_id, { name, domain });
|
||||
|
||||
return ok(res);
|
||||
}
|
||||
|
||||
return unauthorized(res);
|
||||
} else {
|
||||
const website_uuid = uuid();
|
||||
const website = await createWebsite(user_id, { website_uuid, name, domain });
|
||||
|
||||
return ok(res, website);
|
||||
}
|
||||
}
|
||||
|
||||
return res.status(405).end();
|
||||
return methodNotAllowed(res);
|
||||
};
|
||||
|
@ -1,12 +1,31 @@
|
||||
import { getWebsite } from 'lib/db';
|
||||
import { deleteWebsite, getWebsite } from 'lib/db';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
const { user_id, is_admin } = req.auth;
|
||||
const { id } = req.query;
|
||||
const website_id = +id;
|
||||
|
||||
const website = await getWebsite({ website_id: +id });
|
||||
if (req.method === 'GET') {
|
||||
const website = await getWebsite({ website_id });
|
||||
|
||||
return res.status(200).json(website);
|
||||
return ok(res, website);
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
const website = await getWebsite({ website_id });
|
||||
|
||||
if (website.user_id === user_id || is_admin) {
|
||||
await deleteWebsite(website_id);
|
||||
|
||||
return ok(res);
|
||||
}
|
||||
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { getMetrics } from 'lib/db';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok } from 'lib/response';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
@ -17,5 +18,5 @@ export default async (req, res) => {
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
return res.status(200).json(stats);
|
||||
return ok(res, stats);
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import moment from 'moment-timezone';
|
||||
import { getPageviewData } from 'lib/db';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok, badRequest } from 'lib/response';
|
||||
|
||||
const unitTypes = ['month', 'hour', 'day'];
|
||||
|
||||
@ -10,7 +11,7 @@ export default async (req, res) => {
|
||||
const { id, start_at, end_at, unit, tz } = req.query;
|
||||
|
||||
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
||||
return res.status(400).end();
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const start = new Date(+start_at);
|
||||
@ -21,5 +22,5 @@ export default async (req, res) => {
|
||||
getPageviewData(+id, start, end, tz, unit, 'distinct session_id'),
|
||||
]);
|
||||
|
||||
return res.status(200).json({ pageviews, uniques });
|
||||
return ok(res, { pageviews, uniques });
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { getRankings } from 'lib/db';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok, badRequest } from 'lib/response';
|
||||
|
||||
const sessionColumns = ['browser', 'os', 'device', 'country'];
|
||||
const pageviewColumns = ['url', 'referrer'];
|
||||
@ -10,12 +11,12 @@ export default async (req, res) => {
|
||||
const { id, type, start_at, end_at } = req.query;
|
||||
|
||||
if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) {
|
||||
return res.status(400).end();
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const table = sessionColumns.includes(type) ? 'session' : 'pageview';
|
||||
|
||||
const rankings = await getRankings(+id, new Date(+start_at), new Date(+end_at), type, table);
|
||||
|
||||
return res.status(200).json(rankings);
|
||||
return ok(res, rankings);
|
||||
};
|
||||
|
@ -1,13 +1,21 @@
|
||||
import Head from 'next/head';
|
||||
import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from 'components/layout/Layout';
|
||||
|
||||
export default function Test({ websiteId }) {
|
||||
export default function Test() {
|
||||
const router = useRouter();
|
||||
const { id } = router.query;
|
||||
|
||||
if (!id) {
|
||||
return <h1>No id query specified.</h1>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{typeof window !== 'undefined' && (
|
||||
<script async defer data-website-id={websiteId} src="/umami.js" />
|
||||
<script async defer data-website-id={id} src="/umami.js" />
|
||||
)}
|
||||
</Head>
|
||||
<Layout>
|
||||
@ -17,17 +25,17 @@ export default function Test({ websiteId }) {
|
||||
trigger page views. Clicking on the button should trigger an event.
|
||||
</p>
|
||||
<h2>Page links</h2>
|
||||
<Link href="?q=1">
|
||||
<Link href={`?id=${id}&q=1`}>
|
||||
<a>Page One</a>
|
||||
</Link>
|
||||
<br />
|
||||
<Link href="?q=2">
|
||||
<Link href={`?id=${id}&q=2`}>
|
||||
<a>Page Two</a>
|
||||
</Link>
|
||||
<h2>Events</h2>
|
||||
<button
|
||||
id="primary-button"
|
||||
className="otherClass umami--click--primary-button"
|
||||
className="otherClass umami--click--primary-button align-self-start"
|
||||
type="button"
|
||||
>
|
||||
Button
|
||||
@ -36,11 +44,3 @@ export default function Test({ websiteId }) {
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getStaticProps() {
|
||||
return {
|
||||
props: {
|
||||
websiteId: process.env.TEST_WEBSITE_ID,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -32,7 +32,7 @@ create table session (
|
||||
|
||||
create table pageview (
|
||||
view_id serial primary key,
|
||||
website_id int not null references website(website_id),
|
||||
website_id int not null references website(website_id) on delete cascade,
|
||||
session_id int not null references session(session_id) on delete cascade,
|
||||
created_at timestamp with time zone default current_timestamp,
|
||||
url varchar(500) not null,
|
||||
@ -41,7 +41,7 @@ create table pageview (
|
||||
|
||||
create table event (
|
||||
event_id serial primary key,
|
||||
website_id int not null references website(website_id),
|
||||
website_id int not null references website(website_id) on delete cascade,
|
||||
session_id int not null references session(session_id) on delete cascade,
|
||||
created_at timestamp with time zone default current_timestamp,
|
||||
url varchar(500) not null,
|
||||
|
@ -27,5 +27,8 @@
|
||||
--grid-size-large: 992px;
|
||||
--grid-size-xlarge: 1140px;
|
||||
|
||||
--color-error: #e34850;
|
||||
--red400: #e34850;
|
||||
--red500: #d7373f;
|
||||
--red600: #c9252d;
|
||||
--red700: #bb121a;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user