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