Merge branch 'dev' into patch-1
commit
ee3c9debb2
|
@ -4,7 +4,12 @@
|
||||||
"es2020": true,
|
"es2020": true,
|
||||||
"node": true
|
"node": true
|
||||||
},
|
},
|
||||||
"extends": ["eslint:recommended", "plugin:prettier/recommended", "next"],
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:prettier/recommended",
|
||||||
|
"plugin:import/recommended",
|
||||||
|
"next"
|
||||||
|
],
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"ecmaFeatures": {
|
"ecmaFeatures": {
|
||||||
"jsx": true
|
"jsx": true
|
||||||
|
@ -12,7 +17,27 @@
|
||||||
"ecmaVersion": 11,
|
"ecmaVersion": 11,
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
|
"settings": {
|
||||||
|
"import/resolver": {
|
||||||
|
"alias": {
|
||||||
|
"map": [
|
||||||
|
["assets", "./assets"],
|
||||||
|
["components", "./components"],
|
||||||
|
["db", "./db"],
|
||||||
|
["hooks", "./hooks"],
|
||||||
|
["lang", "./lang"],
|
||||||
|
["lib", "./lib"],
|
||||||
|
["public", "./public"],
|
||||||
|
["queries", "./queries"],
|
||||||
|
["store", "./store"],
|
||||||
|
["styles", "./styles"]
|
||||||
|
],
|
||||||
|
"extensions": [".ts", ".js", ".jsx", ".json"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"rules": {
|
"rules": {
|
||||||
|
"no-console": "error",
|
||||||
"react/display-name": "off",
|
"react/display-name": "off",
|
||||||
"react/react-in-jsx-scope": "off",
|
"react/react-in-jsx-scope": "off",
|
||||||
"react/prop-types": "off",
|
"react/prop-types": "off",
|
||||||
|
|
|
@ -7,6 +7,7 @@ on: [push]
|
||||||
|
|
||||||
env:
|
env:
|
||||||
DATABASE_TYPE: postgresql
|
DATABASE_TYPE: postgresql
|
||||||
|
SKIP_DB_CHECK: 1
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
|
|
@ -35,3 +35,6 @@ yarn-error.log*
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
|
||||||
|
*.dev.yml
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ ENV BASE_PATH $BASE_PATH
|
||||||
|
|
||||||
ENV NEXT_TELEMETRY_DISABLED 1
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
RUN yarn build
|
RUN yarn build-docker
|
||||||
|
|
||||||
# Production image, copy all the files and run next
|
# Production image, copy all the files and run next
|
||||||
FROM node:16-alpine AS runner
|
FROM node:16-alpine AS runner
|
||||||
|
|
|
@ -18,6 +18,10 @@ export const filterOptions = [
|
||||||
),
|
),
|
||||||
value: '24hour',
|
value: '24hour',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.yesterday" defaultMessage="Yesterday" />,
|
||||||
|
value: '-1day',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
|
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
|
||||||
value: '1week',
|
value: '1week',
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Link from 'next/link';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { safeDecodeURI } from 'next-basics';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import { safeDecodeURI } from 'lib/url';
|
|
||||||
import Icon from './Icon';
|
|
||||||
import External from 'assets/arrow-up-right-from-square.svg';
|
import External from 'assets/arrow-up-right-from-square.svg';
|
||||||
|
import Icon from './Icon';
|
||||||
import styles from './FilterLink.module.css';
|
import styles from './FilterLink.module.css';
|
||||||
|
|
||||||
export default function FilterLink({ id, value, label, externalUrl }) {
|
export default function FilterLink({ id, value, label, externalUrl }) {
|
||||||
|
@ -25,7 +25,7 @@ export default function FilterLink({ id, value, label, externalUrl }) {
|
||||||
</a>
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
{externalUrl && (
|
{externalUrl && (
|
||||||
<a href={externalUrl} target="_blank" rel="noreferrer noopener" className={styles.link}>
|
<a className={styles.link} href={externalUrl} target="_blank" rel="noreferrer noopener">
|
||||||
<Icon icon={<External />} className={styles.icon} />
|
<Icon icon={<External />} className={styles.icon} />
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -3,9 +3,9 @@ import PropTypes from 'prop-types';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Loading.module.css';
|
import styles from './Loading.module.css';
|
||||||
|
|
||||||
function Loading({ className }) {
|
function Loading({ className, overlay = false }) {
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.loading, className)}>
|
<div className={classNames(styles.loading, { [styles.overlay]: overlay }, className)}>
|
||||||
<div />
|
<div />
|
||||||
<div />
|
<div />
|
||||||
<div />
|
<div />
|
||||||
|
@ -15,6 +15,7 @@ function Loading({ className }) {
|
||||||
|
|
||||||
Loading.propTypes = {
|
Loading.propTypes = {
|
||||||
className: PropTypes.string,
|
className: PropTypes.string,
|
||||||
|
overlay: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Loading;
|
export default Loading;
|
||||||
|
|
|
@ -21,6 +21,14 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading.overlay {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
z-index: 10;
|
||||||
|
background: var(--gray400);
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
.loading div {
|
.loading div {
|
||||||
width: 10px;
|
width: 10px;
|
||||||
height: 10px;
|
height: 10px;
|
||||||
|
@ -30,6 +38,10 @@
|
||||||
animation-fill-mode: both;
|
animation-fill-mode: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading.overlay div {
|
||||||
|
background: var(--gray900);
|
||||||
|
}
|
||||||
|
|
||||||
.loading div + div {
|
.loading div + div {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { setItem } from 'next-basics';
|
||||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import useStore, { checkVersion } from 'store/version';
|
import useStore, { checkVersion } from 'store/version';
|
||||||
import { setItem } from 'lib/web';
|
|
||||||
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
import { REPO_URL, VERSION_CHECK } from 'lib/constants';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import styles from './UpdateNotice.module.css';
|
import styles from './UpdateNotice.module.css';
|
||||||
|
|
|
@ -8,6 +8,7 @@ import FormLayout, {
|
||||||
FormMessage,
|
FormMessage,
|
||||||
FormRow,
|
FormRow,
|
||||||
} from 'components/layout/FormLayout';
|
} from 'components/layout/FormLayout';
|
||||||
|
import Loading from 'components/common/Loading';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
|
||||||
const CONFIRMATION_WORD = 'DELETE';
|
const CONFIRMATION_WORD = 'DELETE';
|
||||||
|
@ -29,8 +30,11 @@ const validate = ({ confirmation }) => {
|
||||||
export default function DeleteForm({ values, onSave, onClose }) {
|
export default function DeleteForm({ values, onSave, onClose }) {
|
||||||
const { del } = useApi();
|
const { del } = useApi();
|
||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
|
const [deleting, setDeleting] = useState(false);
|
||||||
|
|
||||||
const handleSubmit = async ({ type, id }) => {
|
const handleSubmit = async ({ type, id }) => {
|
||||||
|
setDeleting(true);
|
||||||
|
|
||||||
const { ok, data } = await del(`/${type}/${id}`);
|
const { ok, data } = await del(`/${type}/${id}`);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
|
@ -39,11 +43,14 @@ export default function DeleteForm({ values, onSave, onClose }) {
|
||||||
setMessage(
|
setMessage(
|
||||||
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
data || <FormattedMessage id="message.failure" defaultMessage="Something went wrong." />,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
setDeleting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormLayout>
|
<FormLayout>
|
||||||
|
{deleting && <Loading overlay />}
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{ confirmation: '', ...values }}
|
initialValues={{ confirmation: '', ...values }}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { Formik, Form, Field } from 'formik';
|
import { Formik, Form, Field } from 'formik';
|
||||||
|
import { setItem } from 'next-basics';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import FormLayout, {
|
import FormLayout, {
|
||||||
|
@ -11,7 +12,6 @@ import FormLayout, {
|
||||||
} from 'components/layout/FormLayout';
|
} from 'components/layout/FormLayout';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
import { setItem } from 'lib/web';
|
|
||||||
import { AUTH_TOKEN } from 'lib/constants';
|
import { AUTH_TOKEN } from 'lib/constants';
|
||||||
import { setUser } from 'store/app';
|
import { setUser } from 'store/app';
|
||||||
import Logo from 'assets/logo.svg';
|
import Logo from 'assets/logo.svg';
|
||||||
|
|
|
@ -9,7 +9,7 @@ import HamburgerButton from 'components/common/HamburgerButton';
|
||||||
import UpdateNotice from 'components/common/UpdateNotice';
|
import UpdateNotice from 'components/common/UpdateNotice';
|
||||||
import UserButton from 'components/settings/UserButton';
|
import UserButton from 'components/settings/UserButton';
|
||||||
import { HOMEPAGE_URL } from 'lib/constants';
|
import { HOMEPAGE_URL } from 'lib/constants';
|
||||||
import useConfig from '/hooks/useConfig';
|
import useConfig from 'hooks/useConfig';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
import Logo from 'assets/logo.svg';
|
import Logo from 'assets/logo.svg';
|
||||||
import styles from './Header.module.css';
|
import styles from './Header.module.css';
|
||||||
|
|
|
@ -1,26 +1,6 @@
|
||||||
import React from 'react';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import styles from './Page.module.css';
|
import styles from './Page.module.css';
|
||||||
|
|
||||||
export default class Page extends React.Component {
|
export default function Page({ className, children }) {
|
||||||
getSnapshotBeforeUpdate() {
|
return <div className={classNames(styles.page, className)}>{children}</div>;
|
||||||
if (window.pageXOffset === 0 && window.pageYOffset === 0) return null;
|
|
||||||
|
|
||||||
// Return the scrolled position as the snapshot value
|
|
||||||
return { x: window.pageXOffset, y: window.pageYOffset };
|
|
||||||
}
|
|
||||||
|
|
||||||
/* eslint-disable no-unused-vars */
|
|
||||||
componentDidUpdate(prevProps, prevState, snapshot) {
|
|
||||||
if (snapshot !== null) {
|
|
||||||
// Restore the scrolled position after re-rendering
|
|
||||||
window.scrollTo(snapshot.x, snapshot.y);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* eslint-enable no-unused-vars */
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { className, children } = this.props;
|
|
||||||
return <div className={classNames(styles.page, className)}>{children}</div>;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
|
import { safeDecodeURI } from 'next-basics';
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import Times from 'assets/times.svg';
|
import Times from 'assets/times.svg';
|
||||||
import { safeDecodeURI } from 'lib/url';
|
|
||||||
import styles from './FilterTags.module.css';
|
import styles from './FilterTags.module.css';
|
||||||
|
|
||||||
export default function FilterTags({ params, onClick }) {
|
export default function FilterTags({ params, onClick }) {
|
||||||
|
|
|
@ -29,9 +29,9 @@ const MetricCard = ({
|
||||||
: !reverseColors
|
: !reverseColors
|
||||||
? styles.negative
|
? styles.negative
|
||||||
: styles.positive
|
: styles.positive
|
||||||
}`}
|
} ${change >= 0 ? styles.plusSign : ''}`}
|
||||||
>
|
>
|
||||||
{changeProps.x.interpolate(x => `${change >= 0 ? '+' : ''}${format(x)}`)}
|
{changeProps.x.interpolate(x => format(x))}
|
||||||
</animated.span>
|
</animated.span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,3 +37,7 @@
|
||||||
.change.negative {
|
.change.negative {
|
||||||
color: var(--red500);
|
color: var(--red500);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.change.plusSign::before {
|
||||||
|
content: '+';
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useIntl, defineMessages } from 'react-intl';
|
import { useIntl, defineMessages } from 'react-intl';
|
||||||
import MetricsTable from './MetricsTable';
|
import { safeDecodeURI } from 'next-basics';
|
||||||
import Tag from 'components/common/Tag';
|
import Tag from 'components/common/Tag';
|
||||||
|
import FilterButtons from 'components/common/FilterButtons';
|
||||||
import { paramFilter } from 'lib/filters';
|
import { paramFilter } from 'lib/filters';
|
||||||
import { safeDecodeURI } from 'lib/url';
|
import MetricsTable from './MetricsTable';
|
||||||
import FilterButtons from '../common/FilterButtons';
|
|
||||||
|
|
||||||
const FILTER_COMBINED = 0;
|
const FILTER_COMBINED = 0;
|
||||||
const FILTER_RAW = 1;
|
const FILTER_RAW = 1;
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { defineMessages, useIntl } from 'react-intl';
|
||||||
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Draggable, Droppable } from 'react-beautiful-dnd';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import Button from 'components/common/Button';
|
import Button from 'components/common/Button';
|
||||||
import { sortArrayByMap } from 'lib/array';
|
import { firstBy } from 'thenby';
|
||||||
import useDashboard, { saveDashboard } from 'store/dashboard';
|
import useDashboard, { saveDashboard } from 'store/dashboard';
|
||||||
import styles from './DashboardEdit.module.css';
|
import styles from './DashboardEdit.module.css';
|
||||||
|
|
||||||
|
@ -21,8 +21,13 @@ export default function DashboardEdit({ websites }) {
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
const [order, setOrder] = useState(websiteOrder || []);
|
const [order, setOrder] = useState(websiteOrder || []);
|
||||||
|
|
||||||
const ordered = useMemo(() => sortArrayByMap(websites, order, 'website_id'), [websites, order]);
|
const ordered = useMemo(
|
||||||
|
() =>
|
||||||
|
websites
|
||||||
|
.map(website => ({ ...website, order: order.indexOf(website.website_id) }))
|
||||||
|
.sort(firstBy('order')),
|
||||||
|
[websites, order],
|
||||||
|
);
|
||||||
|
|
||||||
function handleWebsiteDrag({ destination, source }) {
|
function handleWebsiteDrag({ destination, source }) {
|
||||||
if (!destination || destination.index === source.index) return;
|
if (!destination || destination.index === source.index) return;
|
||||||
|
|
|
@ -28,8 +28,6 @@ export default function TestConsole() {
|
||||||
const website = data.find(({ website_id }) => website_id === +websiteId);
|
const website = data.find(({ website_id }) => website_id === +websiteId);
|
||||||
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value;
|
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value;
|
||||||
|
|
||||||
console.log({ websiteId, data, options, website });
|
|
||||||
|
|
||||||
function handleSelect(value) {
|
function handleSelect(value) {
|
||||||
router.push(`/console/${value}`);
|
router.push(`/console/${value}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,8 +6,8 @@ import EmptyPlaceholder from 'components/common/EmptyPlaceholder';
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
import styles from './WebsiteList.module.css';
|
import styles from './WebsiteList.module.css';
|
||||||
import useDashboard from 'store/dashboard';
|
import useDashboard from 'store/dashboard';
|
||||||
import { sortArrayByMap } from 'lib/array';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { firstBy } from 'thenby';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
noWebsites: {
|
noWebsites: {
|
||||||
|
@ -24,10 +24,11 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||||
const { websiteOrder } = useDashboard();
|
const { websiteOrder } = useDashboard();
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
console.log({ websiteOrder });
|
|
||||||
|
|
||||||
const ordered = useMemo(
|
const ordered = useMemo(
|
||||||
() => sortArrayByMap(websites, websiteOrder, 'website_id'),
|
() =>
|
||||||
|
websites
|
||||||
|
.map(website => ({ ...website, order: websiteOrder.indexOf(website.website_id) || 0 }))
|
||||||
|
.sort(firstBy('order')),
|
||||||
[websites, websiteOrder],
|
[websites, websiteOrder],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import { FormattedMessage } from 'react-intl';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { removeItem } from 'next-basics';
|
||||||
import MenuButton from 'components/common/MenuButton';
|
import MenuButton from 'components/common/MenuButton';
|
||||||
import Icon from 'components/common/Icon';
|
import Icon from 'components/common/Icon';
|
||||||
import User from 'assets/user.svg';
|
import User from 'assets/user.svg';
|
||||||
import styles from './UserButton.module.css';
|
import styles from './UserButton.module.css';
|
||||||
import { removeItem } from 'lib/web';
|
|
||||||
import { AUTH_TOKEN } from 'lib/constants';
|
import { AUTH_TOKEN } from 'lib/constants';
|
||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,130 @@
|
||||||
|
SET allow_experimental_object_type = 1;
|
||||||
|
|
||||||
|
-- Create Pageview
|
||||||
|
CREATE TABLE pageview
|
||||||
|
(
|
||||||
|
website_id UInt32,
|
||||||
|
session_uuid UUID,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
url String,
|
||||||
|
referrer String
|
||||||
|
)
|
||||||
|
engine = MergeTree PRIMARY KEY (session_uuid, created_at)
|
||||||
|
ORDER BY (session_uuid, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
CREATE TABLE pageview_queue (
|
||||||
|
website_id UInt32,
|
||||||
|
session_uuid UUID,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
url String,
|
||||||
|
referrer String
|
||||||
|
)
|
||||||
|
ENGINE = Kafka
|
||||||
|
SETTINGS kafka_broker_list = 'kafka1:19092,kafka2:19093,kafka3:19094', -- input broker list
|
||||||
|
kafka_topic_list = 'pageview',
|
||||||
|
kafka_group_name = 'pageview_consumer_group',
|
||||||
|
kafka_format = 'JSONEachRow',
|
||||||
|
kafka_max_block_size = 1048576,
|
||||||
|
kafka_skip_broken_messages = 1;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW pageview_queue_mv TO pageview AS
|
||||||
|
SELECT website_id,
|
||||||
|
session_uuid,
|
||||||
|
created_at,
|
||||||
|
url,
|
||||||
|
referrer
|
||||||
|
FROM pageview_queue;
|
||||||
|
|
||||||
|
-- Create Session
|
||||||
|
CREATE TABLE session
|
||||||
|
(
|
||||||
|
session_uuid UUID,
|
||||||
|
website_id UInt32,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
browser LowCardinality(String),
|
||||||
|
os LowCardinality(String),
|
||||||
|
device LowCardinality(String),
|
||||||
|
screen LowCardinality(String),
|
||||||
|
language LowCardinality(String),
|
||||||
|
country LowCardinality(String)
|
||||||
|
)
|
||||||
|
engine = MergeTree PRIMARY KEY (session_uuid, created_at)
|
||||||
|
ORDER BY (session_uuid, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
CREATE TABLE session_queue (
|
||||||
|
session_uuid UUID,
|
||||||
|
website_id UInt32,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
browser LowCardinality(String),
|
||||||
|
os LowCardinality(String),
|
||||||
|
device LowCardinality(String),
|
||||||
|
screen LowCardinality(String),
|
||||||
|
language LowCardinality(String),
|
||||||
|
country LowCardinality(String)
|
||||||
|
)
|
||||||
|
ENGINE = Kafka
|
||||||
|
SETTINGS kafka_broker_list = 'kafka1:19092,kafka2:19093,kafka3:19094', -- input broker list
|
||||||
|
kafka_topic_list = 'session',
|
||||||
|
kafka_group_name = 'session_consumer_group',
|
||||||
|
kafka_format = 'JSONEachRow',
|
||||||
|
kafka_max_block_size = 1048576,
|
||||||
|
kafka_skip_broken_messages = 1;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW session_queue_mv TO session AS
|
||||||
|
SELECT session_uuid,
|
||||||
|
website_id,
|
||||||
|
created_at,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country
|
||||||
|
FROM session_queue;
|
||||||
|
|
||||||
|
-- Create event
|
||||||
|
CREATE TABLE event
|
||||||
|
(
|
||||||
|
event_uuid UUID,
|
||||||
|
website_id UInt32,
|
||||||
|
session_uuid UUID,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
url String,
|
||||||
|
event_name String,
|
||||||
|
event_data JSON
|
||||||
|
)
|
||||||
|
engine = MergeTree PRIMARY KEY (event_uuid, created_at)
|
||||||
|
ORDER BY (event_uuid, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
CREATE TABLE event_queue (
|
||||||
|
event_uuid UUID,
|
||||||
|
website_id UInt32,
|
||||||
|
session_uuid UUID,
|
||||||
|
created_at DateTime('UTC'),
|
||||||
|
url String,
|
||||||
|
event_name String,
|
||||||
|
event_data String
|
||||||
|
)
|
||||||
|
ENGINE = Kafka
|
||||||
|
SETTINGS kafka_broker_list = 'kafka1:19092,kafka2:19093,kafka3:19094', -- input broker list
|
||||||
|
kafka_topic_list = 'event',
|
||||||
|
kafka_group_name = 'event_consumer_group',
|
||||||
|
kafka_format = 'JSONEachRow',
|
||||||
|
kafka_max_block_size = 1048576,
|
||||||
|
kafka_skip_broken_messages = 1;
|
||||||
|
|
||||||
|
CREATE MATERIALIZED VIEW event_queue_mv TO event AS
|
||||||
|
SELECT event_uuid,
|
||||||
|
website_id,
|
||||||
|
session_uuid,
|
||||||
|
created_at,
|
||||||
|
url,
|
||||||
|
event_name,
|
||||||
|
event_data
|
||||||
|
FROM event_queue;
|
|
@ -0,0 +1,35 @@
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE `event` DROP FOREIGN KEY `event_ibfk_2`;
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE `event` DROP FOREIGN KEY `event_ibfk_1`;
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE `pageview` DROP FOREIGN KEY `pageview_ibfk_2`;
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE `pageview` DROP FOREIGN KEY `pageview_ibfk_1`;
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE `session` DROP FOREIGN KEY `session_ibfk_1`;
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE `website` DROP FOREIGN KEY `website_ibfk_1`;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `event` ADD CONSTRAINT `event_session_id_fkey` FOREIGN KEY (`session_id`) REFERENCES `session`(`session_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `event` ADD CONSTRAINT `event_website_id_fkey` FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `pageview` ADD CONSTRAINT `pageview_session_id_fkey` FOREIGN KEY (`session_id`) REFERENCES `session`(`session_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `pageview` ADD CONSTRAINT `pageview_website_id_fkey` FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `session` ADD CONSTRAINT `session_website_id_fkey` FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE `website` ADD CONSTRAINT `website_user_id_fkey` FOREIGN KEY (`user_id`) REFERENCES `account`(`user_id`) ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
@ -24,8 +24,8 @@ model event {
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
event_name String @db.VarChar(50)
|
event_name String @db.VarChar(50)
|
||||||
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade, onUpdate: NoAction, map: "event_ibfk_2")
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction, map: "event_ibfk_1")
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
event_data event_data?
|
event_data event_data?
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([created_at])
|
||||||
|
@ -34,7 +34,7 @@ model event {
|
||||||
}
|
}
|
||||||
|
|
||||||
model event_data {
|
model event_data {
|
||||||
event_data_id Int @id @default(autoincrement())
|
event_data_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||||
event_id Int @unique @db.UnsignedInt
|
event_id Int @unique @db.UnsignedInt
|
||||||
event_data Json
|
event_data Json
|
||||||
event event @relation(fields: [event_id], references: [event_id])
|
event event @relation(fields: [event_id], references: [event_id])
|
||||||
|
@ -47,8 +47,8 @@ model pageview {
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
referrer String? @db.VarChar(500)
|
referrer String? @db.VarChar(500)
|
||||||
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade, onUpdate: NoAction, map: "pageview_ibfk_2")
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction, map: "pageview_ibfk_1")
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([created_at])
|
||||||
@@index([session_id])
|
@@index([session_id])
|
||||||
|
@ -69,7 +69,7 @@ model session {
|
||||||
screen String? @db.VarChar(11)
|
screen String? @db.VarChar(11)
|
||||||
language String? @db.VarChar(35)
|
language String? @db.VarChar(35)
|
||||||
country String? @db.Char(2)
|
country String? @db.Char(2)
|
||||||
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade, onUpdate: NoAction, map: "session_ibfk_1")
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
|
|
||||||
|
@ -85,7 +85,7 @@ model website {
|
||||||
domain String? @db.VarChar(500)
|
domain String? @db.VarChar(500)
|
||||||
share_id String? @unique() @db.VarChar(64)
|
share_id String? @unique() @db.VarChar(64)
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||||
account account @relation(fields: [user_id], references: [user_id], onDelete: Cascade, onUpdate: NoAction, map: "website_ibfk_1")
|
account account @relation(fields: [user_id], references: [user_id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
session session[]
|
session session[]
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "event" DROP CONSTRAINT IF EXISTS "event_session_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "event" DROP CONSTRAINT IF EXISTS "event_website_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "pageview" DROP CONSTRAINT IF EXISTS "pageview_session_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "pageview" DROP CONSTRAINT IF EXISTS "pageview_website_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "session" DROP CONSTRAINT IF EXISTS "session_website_id_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "website" DROP CONSTRAINT IF EXISTS "website_user_id_fkey";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "event" ADD CONSTRAINT "event_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "event" ADD CONSTRAINT "event_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "pageview" ADD CONSTRAINT "pageview_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "pageview" ADD CONSTRAINT "pageview_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "session" ADD CONSTRAINT "session_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "website" ADD CONSTRAINT "website_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
@ -24,8 +24,8 @@ model event {
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
event_name String @db.VarChar(50)
|
event_name String @db.VarChar(50)
|
||||||
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade)
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade)
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
event_data event_data?
|
event_data event_data?
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([created_at])
|
||||||
|
@ -47,8 +47,8 @@ model pageview {
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
referrer String? @db.VarChar(500)
|
referrer String? @db.VarChar(500)
|
||||||
session session @relation(fields: [session_id], references: [session_id], onDelete: Cascade)
|
session session @relation(fields: [session_id], references: [session_id])
|
||||||
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade)
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([created_at])
|
||||||
@@index([session_id])
|
@@index([session_id])
|
||||||
|
@ -69,9 +69,9 @@ model session {
|
||||||
screen String? @db.VarChar(11)
|
screen String? @db.VarChar(11)
|
||||||
language String? @db.VarChar(35)
|
language String? @db.VarChar(35)
|
||||||
country String? @db.Char(2)
|
country String? @db.Char(2)
|
||||||
website website @relation(fields: [website_id], references: [website_id], onDelete: Cascade)
|
website website @relation(fields: [website_id], references: [website_id])
|
||||||
pageview pageview[]
|
|
||||||
event event[]
|
event event[]
|
||||||
|
pageview pageview[]
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([created_at])
|
||||||
@@index([website_id])
|
@@index([website_id])
|
||||||
|
@ -85,10 +85,10 @@ model website {
|
||||||
domain String? @db.VarChar(500)
|
domain String? @db.VarChar(500)
|
||||||
share_id String? @unique @db.VarChar(64)
|
share_id String? @unique @db.VarChar(64)
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||||
account account @relation(fields: [user_id], references: [user_id], onDelete: Cascade)
|
account account @relation(fields: [user_id], references: [user_id])
|
||||||
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
session session[]
|
session session[]
|
||||||
event event[]
|
|
||||||
|
|
||||||
@@index([user_id])
|
@@index([user_id])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get, post, put, del, getItem } from 'lib/web';
|
import { get, post, put, del, getItem } from 'next-basics';
|
||||||
import { AUTH_TOKEN, SHARE_TOKEN_HEADER } from 'lib/constants';
|
import { AUTH_TOKEN, SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||||
import useStore from 'store/app';
|
import useStore from 'store/app';
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'next-basics';
|
||||||
import enUS from 'public/intl/country/en-US.json';
|
import enUS from 'public/intl/country/en-US.json';
|
||||||
|
|
||||||
const countryNames = {
|
const countryNames = {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { useCallback, useMemo } from 'react';
|
import { useCallback, useMemo } from 'react';
|
||||||
import { parseISO } from 'date-fns';
|
import { parseISO } from 'date-fns';
|
||||||
import { getDateRange } from 'lib/date';
|
import { getDateRange } from 'lib/date';
|
||||||
import { getItem, setItem } from 'lib/web';
|
import { getItem, setItem } from 'next-basics';
|
||||||
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
import { DATE_RANGE_CONFIG, DEFAULT_DATE_RANGE } from 'lib/constants';
|
||||||
import useForceUpdate from './useForceUpdate';
|
import useForceUpdate from './useForceUpdate';
|
||||||
import useLocale from './useLocale';
|
import useLocale from './useLocale';
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default function useFetch(url, options = {}, update = []) {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [count, setCount] = useState(0);
|
const [count, setCount] = useState(0);
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
const { params = {}, headers = {}, disabled, delay = 0, interval, onDataLoad } = options;
|
const { params = {}, headers = {}, disabled = false, delay = 0, interval, onDataLoad } = options;
|
||||||
|
|
||||||
async function loadData(params) {
|
async function loadData(params) {
|
||||||
try {
|
try {
|
||||||
|
@ -29,7 +29,9 @@ export default function useFetch(url, options = {}, update = []) {
|
||||||
|
|
||||||
onDataLoad?.(data);
|
onDataLoad?.(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
setError(e);
|
setError(e);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
@ -44,7 +46,7 @@ export default function useFetch(url, options = {}, update = []) {
|
||||||
clearTimeout(id);
|
clearTimeout(id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [url, !!disabled, count, ...update]);
|
}, [url, disabled, count, ...update]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (interval && !disabled) {
|
if (interval && !disabled) {
|
||||||
|
@ -54,7 +56,7 @@ export default function useFetch(url, options = {}, update = []) {
|
||||||
clearInterval(id);
|
clearInterval(id);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [interval, !!disabled]);
|
}, [interval, disabled]);
|
||||||
|
|
||||||
return { ...response, error, loading };
|
return { ...response, error, loading };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get } from 'lib/web';
|
import { get } from 'next-basics';
|
||||||
import enUS from 'public/intl/language/en-US.json';
|
import enUS from 'public/intl/language/en-US.json';
|
||||||
|
|
||||||
const languageNames = {
|
const languageNames = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { get, setItem } from 'lib/web';
|
import { get, setItem } from 'next-basics';
|
||||||
import { LOCALE_CONFIG } from 'lib/constants';
|
import { LOCALE_CONFIG } from 'lib/constants';
|
||||||
import { getDateLocale, getTextDirection } from 'lib/lang';
|
import { getDateLocale, getTextDirection } from 'lib/lang';
|
||||||
import useStore, { setLocale } from 'store/app';
|
import useStore, { setLocale } from 'store/app';
|
||||||
|
@ -48,5 +48,14 @@ export default function useLocale() {
|
||||||
}
|
}
|
||||||
}, [locale]);
|
}, [locale]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const locale = url.searchParams.get('locale');
|
||||||
|
|
||||||
|
if (locale) {
|
||||||
|
saveLocale(locale);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return { locale, saveLocale, messages, dir, dateLocale };
|
return { locale, saveLocale, messages, dir, dateLocale };
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { getQueryString } from 'lib/url';
|
|
||||||
|
function getQueryString(params) {
|
||||||
|
return new URLSearchParams({ ...params }).toString();
|
||||||
|
}
|
||||||
|
|
||||||
export default function usePageQuery() {
|
export default function usePageQuery() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import useStore, { setTheme } from 'store/app';
|
import useStore, { setTheme } from 'store/app';
|
||||||
import { getItem, setItem } from 'lib/web';
|
import { getItem, setItem } from 'next-basics';
|
||||||
import { THEME_CONFIG } from 'lib/constants';
|
import { THEME_CONFIG } from 'lib/constants';
|
||||||
|
|
||||||
const selector = state => state.theme;
|
const selector = state => state.theme;
|
||||||
|
@ -23,5 +23,14 @@ export default function useTheme() {
|
||||||
document.body.setAttribute('data-theme', theme);
|
document.body.setAttribute('data-theme', theme);
|
||||||
}, [theme]);
|
}, [theme]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const theme = url.searchParams.get('theme');
|
||||||
|
|
||||||
|
if (['light', 'dark'].includes(theme)) {
|
||||||
|
saveTheme(theme);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return [theme, saveTheme];
|
return [theme, saveTheme];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useState, useCallback } from 'react';
|
import { useState, useCallback } from 'react';
|
||||||
import { getTimezone } from 'lib/date';
|
import { getTimezone } from 'lib/date';
|
||||||
import { getItem, setItem } from 'lib/web';
|
import { getItem, setItem } from 'next-basics';
|
||||||
import { TIMEZONE_CONFIG } from 'lib/constants';
|
import { TIMEZONE_CONFIG } from 'lib/constants';
|
||||||
|
|
||||||
export default function useTimezone() {
|
export default function useTimezone() {
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"label.username": "Nom d'usuari",
|
"label.username": "Nom d'usuari",
|
||||||
"label.view-details": "Veure els detalls",
|
"label.view-details": "Veure els detalls",
|
||||||
"label.websites": "Llocs web",
|
"label.websites": "Llocs web",
|
||||||
|
"label.yesterday": "Ahir",
|
||||||
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
|
"message.active-users": "{x} {x, plural, one {visitant actual} other {visitants actuals}}",
|
||||||
"message.confirm-delete": "Segur que vols esborrar {target}?",
|
"message.confirm-delete": "Segur que vols esborrar {target}?",
|
||||||
"message.confirm-reset": "Segur que vols restablir les estadístiques de {target}?",
|
"message.confirm-reset": "Segur que vols restablir les estadístiques de {target}?",
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"label.username": "Benutzername",
|
"label.username": "Benutzername",
|
||||||
"label.view-details": "Details anzeigen",
|
"label.view-details": "Details anzeigen",
|
||||||
"label.websites": "Webseiten",
|
"label.websites": "Webseiten",
|
||||||
|
"label.yesterday": "Gestern",
|
||||||
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
|
"message.active-users": "{x} {x, plural, one {aktiver Besucher} other {aktive Besucher}}",
|
||||||
"message.confirm-delete": "Sind Sie sich sicher, {target} zu löschen?",
|
"message.confirm-delete": "Sind Sie sich sicher, {target} zu löschen?",
|
||||||
"message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?",
|
"message.confirm-reset": "Sind Sie sicher, dass Sie die Statistiken von {target} zurücksetzen wollen?",
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"label.username": "Username",
|
"label.username": "Username",
|
||||||
"label.view-details": "View details",
|
"label.view-details": "View details",
|
||||||
"label.websites": "Websites",
|
"label.websites": "Websites",
|
||||||
|
"label.yesterday": "Yesterday",
|
||||||
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
|
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
|
||||||
"message.confirm-delete": "Are you sure you want to delete {target}?",
|
"message.confirm-delete": "Are you sure you want to delete {target}?",
|
||||||
"message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
|
"message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"label.username": "Username",
|
"label.username": "Username",
|
||||||
"label.view-details": "View details",
|
"label.view-details": "View details",
|
||||||
"label.websites": "Websites",
|
"label.websites": "Websites",
|
||||||
|
"label.yesterday": "Yesterday",
|
||||||
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
|
"message.active-users": "{x} current {x, plural, one {visitor} other {visitors}}",
|
||||||
"message.confirm-delete": "Are you sure you want to delete {target}?",
|
"message.confirm-delete": "Are you sure you want to delete {target}?",
|
||||||
"message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
|
"message.confirm-reset": "Are you sure you want to reset {target}'s statistics?",
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"label.username": "Nombre de usuario",
|
"label.username": "Nombre de usuario",
|
||||||
"label.view-details": "Ver detalles",
|
"label.view-details": "Ver detalles",
|
||||||
"label.websites": "Sitios",
|
"label.websites": "Sitios",
|
||||||
|
"label.yesterday": "Ayer",
|
||||||
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
"message.active-users": "{x} {x, plural, one {activo} other {activos}}",
|
||||||
"message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?",
|
"message.confirm-delete": "¿Estás seguro(a) de querer eliminar {target}?",
|
||||||
"message.confirm-reset": "¿Seguro que deseas restablecer las estadísticas de {target}?",
|
"message.confirm-reset": "¿Seguro que deseas restablecer las estadísticas de {target}?",
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
"label.administrator": "Administrateur",
|
"label.administrator": "Administrateur",
|
||||||
"label.all": "Tout",
|
"label.all": "Tout",
|
||||||
"label.all-time": "Toutes les données",
|
"label.all-time": "Toutes les données",
|
||||||
"label.all-websites": "Tous les sites web",
|
"label.all-websites": "Tous les sites",
|
||||||
"label.back": "Retour",
|
"label.back": "Retour",
|
||||||
"label.cancel": "Annuler",
|
"label.cancel": "Annuler",
|
||||||
"label.change-password": "Changer le mot de passe",
|
"label.change-password": "Changer le mot de passe",
|
||||||
|
@ -27,7 +27,7 @@
|
||||||
"label.enable-share-url": "Activer l'URL de partage",
|
"label.enable-share-url": "Activer l'URL de partage",
|
||||||
"label.invalid": "Invalide",
|
"label.invalid": "Invalide",
|
||||||
"label.invalid-domain": "Domaine invalide",
|
"label.invalid-domain": "Domaine invalide",
|
||||||
"label.language": "Langage",
|
"label.language": "Langue",
|
||||||
"label.last-days": "{x} derniers jours",
|
"label.last-days": "{x} derniers jours",
|
||||||
"label.last-hours": "{x} dernières heures",
|
"label.last-hours": "{x} dernières heures",
|
||||||
"label.logged-in-as": "Connecté en tant que {username}",
|
"label.logged-in-as": "Connecté en tant que {username}",
|
||||||
|
@ -52,7 +52,7 @@
|
||||||
"label.share-url": "Partager l'URL",
|
"label.share-url": "Partager l'URL",
|
||||||
"label.single-day": "Journée",
|
"label.single-day": "Journée",
|
||||||
"label.theme": "Thème",
|
"label.theme": "Thème",
|
||||||
"label.this-month": "Ce mois ci",
|
"label.this-month": "Ce mois",
|
||||||
"label.this-week": "Cette semaine",
|
"label.this-week": "Cette semaine",
|
||||||
"label.this-year": "Cette année",
|
"label.this-year": "Cette année",
|
||||||
"label.timezone": "Fuseau horaire",
|
"label.timezone": "Fuseau horaire",
|
||||||
|
@ -62,12 +62,13 @@
|
||||||
"label.username": "Nom d'utilisateur",
|
"label.username": "Nom d'utilisateur",
|
||||||
"label.view-details": "Voir les details",
|
"label.view-details": "Voir les details",
|
||||||
"label.websites": "Sites",
|
"label.websites": "Sites",
|
||||||
|
"label.yesterday": "Hier",
|
||||||
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
|
"message.active-users": "{x} {x, plural, one {visiteur} other {visiteurs}} actuellement",
|
||||||
"message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?",
|
"message.confirm-delete": "Êtes-vous sûr de vouloir supprimer {target} ?",
|
||||||
"message.confirm-reset": "Êtes-vous sûr de vouloir réinistialiser les statistiques de {target} ?",
|
"message.confirm-reset": "Êtes-vous sûr de vouloir réinitialiser les statistiques de {target} ?",
|
||||||
"message.copied": "Copié !",
|
"message.copied": "Copié !",
|
||||||
"message.delete-warning": "Toutes les données associées seront également supprimées.",
|
"message.delete-warning": "Toutes les données associées seront également supprimées.",
|
||||||
"message.edit-dashboard": "Modifier l'ordre des sites",
|
"message.edit-dashboard": "Modifier le tableau de bord",
|
||||||
"message.failure": "Un problème est survenu.",
|
"message.failure": "Un problème est survenu.",
|
||||||
"message.get-share-url": "Obtenir l'URL de partage",
|
"message.get-share-url": "Obtenir l'URL de partage",
|
||||||
"message.get-tracking-code": "Obtenir le code de suivi",
|
"message.get-tracking-code": "Obtenir le code de suivi",
|
||||||
|
@ -76,14 +77,14 @@
|
||||||
"message.log.visitor": "Visiteur de {country} utilisant {browser} sur {os} {device}",
|
"message.log.visitor": "Visiteur de {country} utilisant {browser} sur {os} {device}",
|
||||||
"message.new-version-available": "Une nouvelle version de umami {version} est disponible !",
|
"message.new-version-available": "Une nouvelle version de umami {version} est disponible !",
|
||||||
"message.no-data-available": "Pas de données disponibles.",
|
"message.no-data-available": "Pas de données disponibles.",
|
||||||
"message.no-websites-configured": "Vous n'avez configuré aucun site Web.",
|
"message.no-websites-configured": "Vous n'avez configuré aucun site.",
|
||||||
"message.page-not-found": "Page non trouvée.",
|
"message.page-not-found": "Page non trouvée.",
|
||||||
"message.powered-by": "Propulsé par {name}",
|
"message.powered-by": "Propulsé par {name}",
|
||||||
"message.reset-warning": "Toutes les statistiques pour ce site seront supprimés, mais votre code de suivi restera intact.",
|
"message.reset-warning": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact.",
|
||||||
"message.save-success": "Enregistré avec succès.",
|
"message.save-success": "Enregistré avec succès.",
|
||||||
"message.share-url": "Ceci est l'URL partagée pour {target}.",
|
"message.share-url": "Ceci est l'URL partagée pour {target}.",
|
||||||
"message.toggle-charts": "Afficher/Masquer les graphiques",
|
"message.toggle-charts": "Afficher/Masquer les graphiques",
|
||||||
"message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site Web.",
|
"message.track-stats": "Pour suivre les statistiques de {target}, placez le code suivant dans la section {head} de votre site.",
|
||||||
"message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.",
|
"message.type-delete": "Tapez {delete} dans la case ci-dessous pour confirmer.",
|
||||||
"message.type-reset": "Tapez {reset} dans la case ci-dessous pour confirmer.",
|
"message.type-reset": "Tapez {reset} dans la case ci-dessous pour confirmer.",
|
||||||
"metrics.actions": "Actions",
|
"metrics.actions": "Actions",
|
||||||
|
@ -99,13 +100,13 @@
|
||||||
"metrics.events": "Événements",
|
"metrics.events": "Événements",
|
||||||
"metrics.filter.combined": "Combiné",
|
"metrics.filter.combined": "Combiné",
|
||||||
"metrics.filter.raw": "Brut",
|
"metrics.filter.raw": "Brut",
|
||||||
"metrics.languages": "Langages",
|
"metrics.languages": "Langues",
|
||||||
"metrics.operating-systems": "Systèmes d'exploitation",
|
"metrics.operating-systems": "Systèmes d'exploitation",
|
||||||
"metrics.page-views": "Pages vues",
|
"metrics.page-views": "Pages vues",
|
||||||
"metrics.pages": "Pages",
|
"metrics.pages": "Pages",
|
||||||
"metrics.query-parameters": "Query parameters",
|
"metrics.query-parameters": "Paramètres d'URL",
|
||||||
"metrics.referrers": "Sources",
|
"metrics.referrers": "Sources",
|
||||||
"metrics.screens": "Tailles d'écran",
|
"metrics.screens": "Résolutions d'écran",
|
||||||
"metrics.unique-visitors": "Visiteurs uniques",
|
"metrics.unique-visitors": "Visiteurs uniques",
|
||||||
"metrics.views": "Vues",
|
"metrics.views": "Vues",
|
||||||
"metrics.visitors": "Visiteurs"
|
"metrics.visitors": "Visiteurs"
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"label.username": "Nome utente",
|
"label.username": "Nome utente",
|
||||||
"label.view-details": "Vedi dettagli",
|
"label.view-details": "Vedi dettagli",
|
||||||
"label.websites": "Siti web",
|
"label.websites": "Siti web",
|
||||||
|
"label.yesterday": "Ieri",
|
||||||
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
|
"message.active-users": "{x} {x, plural, one {visitatore} other {visitatori}} online",
|
||||||
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
|
"message.confirm-delete": "Sei sicuro di voler eliminare {target}?",
|
||||||
"message.confirm-reset": "Sei sicuro di voler azzerare le statistiche di {target}?",
|
"message.confirm-reset": "Sei sicuro di voler azzerare le statistiche di {target}?",
|
||||||
|
|
|
@ -67,7 +67,7 @@
|
||||||
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
|
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
|
||||||
"message.copied": "复制成功!",
|
"message.copied": "复制成功!",
|
||||||
"message.delete-warning": "所有相关数据将会被删除。",
|
"message.delete-warning": "所有相关数据将会被删除。",
|
||||||
"message.edit-dashboard": "Edit dashboard",
|
"message.edit-dashboard": "编辑仪表板",
|
||||||
"message.failure": "出现错误。",
|
"message.failure": "出现错误。",
|
||||||
"message.get-share-url": "获取共享链接",
|
"message.get-share-url": "获取共享链接",
|
||||||
"message.get-tracking-code": "获取跟踪代码",
|
"message.get-tracking-code": "获取跟踪代码",
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
"metrics.operating-systems": "操作系统",
|
"metrics.operating-systems": "操作系统",
|
||||||
"metrics.page-views": "页面浏览量",
|
"metrics.page-views": "页面浏览量",
|
||||||
"metrics.pages": "网页",
|
"metrics.pages": "网页",
|
||||||
"metrics.query-parameters": "Query parameters",
|
"metrics.query-parameters": "查询参数",
|
||||||
"metrics.referrers": "来源域名",
|
"metrics.referrers": "来源域名",
|
||||||
"metrics.screens": "屏幕尺寸",
|
"metrics.screens": "屏幕尺寸",
|
||||||
"metrics.unique-visitors": "独立访客",
|
"metrics.unique-visitors": "独立访客",
|
||||||
|
|
|
@ -27,7 +27,7 @@
|
||||||
"label.enable-share-url": "啟用分享連結",
|
"label.enable-share-url": "啟用分享連結",
|
||||||
"label.invalid": "無效輸入",
|
"label.invalid": "無效輸入",
|
||||||
"label.invalid-domain": "無效域名",
|
"label.invalid-domain": "無效域名",
|
||||||
"label.language": "Language",
|
"label.language": "語言",
|
||||||
"label.last-days": "最近 {x} 天",
|
"label.last-days": "最近 {x} 天",
|
||||||
"label.last-hours": "最近 {x} 小時",
|
"label.last-hours": "最近 {x} 小時",
|
||||||
"label.logged-in-as": "用戶名: {username}",
|
"label.logged-in-as": "用戶名: {username}",
|
||||||
|
@ -67,7 +67,7 @@
|
||||||
"message.confirm-reset": "您確定要重置 {target} 的數據嗎?",
|
"message.confirm-reset": "您確定要重置 {target} 的數據嗎?",
|
||||||
"message.copied": "複製成功!",
|
"message.copied": "複製成功!",
|
||||||
"message.delete-warning": "所有相關數據將會被刪除。",
|
"message.delete-warning": "所有相關數據將會被刪除。",
|
||||||
"message.edit-dashboard": "Edit dashboard",
|
"message.edit-dashboard": "編輯管理面板",
|
||||||
"message.failure": "出現錯誤。",
|
"message.failure": "出現錯誤。",
|
||||||
"message.get-share-url": "獲得分享連結",
|
"message.get-share-url": "獲得分享連結",
|
||||||
"message.get-tracking-code": "獲得追蹤代碼",
|
"message.get-tracking-code": "獲得追蹤代碼",
|
||||||
|
@ -103,7 +103,7 @@
|
||||||
"metrics.operating-systems": "操作系統",
|
"metrics.operating-systems": "操作系統",
|
||||||
"metrics.page-views": "網頁流量",
|
"metrics.page-views": "網頁流量",
|
||||||
"metrics.pages": "網頁",
|
"metrics.pages": "網頁",
|
||||||
"metrics.query-parameters": "Query parameters",
|
"metrics.query-parameters": "查詢參數",
|
||||||
"metrics.referrers": "指入域名",
|
"metrics.referrers": "指入域名",
|
||||||
"metrics.screens": "屏幕尺寸",
|
"metrics.screens": "屏幕尺寸",
|
||||||
"metrics.unique-visitors": "獨立訪客",
|
"metrics.unique-visitors": "獨立訪客",
|
||||||
|
|
15
lib/auth.js
15
lib/auth.js
|
@ -1,20 +1,27 @@
|
||||||
import { parseSecureToken, parseToken } from './crypto';
|
import { parseSecureToken, parseToken, getItem } from 'next-basics';
|
||||||
import { SHARE_TOKEN_HEADER } from './constants';
|
import { AUTH_TOKEN, SHARE_TOKEN_HEADER } from './constants';
|
||||||
import { getWebsiteById } from 'queries';
|
import { getWebsiteById } from 'queries';
|
||||||
|
import { secret } from './crypto';
|
||||||
|
|
||||||
export async function getAuthToken(req) {
|
export async function getAuthToken(req) {
|
||||||
try {
|
try {
|
||||||
const token = req.headers.authorization;
|
const token = req.headers.authorization;
|
||||||
|
|
||||||
return parseSecureToken(token.split(' ')[1]);
|
return parseSecureToken(token.split(' ')[1], secret());
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getAuthHeader() {
|
||||||
|
const token = getItem(AUTH_TOKEN);
|
||||||
|
|
||||||
|
return token ? { authorization: `Bearer ${token}` } : {};
|
||||||
|
}
|
||||||
|
|
||||||
export async function isValidToken(token, validation) {
|
export async function isValidToken(token, validation) {
|
||||||
try {
|
try {
|
||||||
const result = await parseToken(token);
|
const result = parseToken(token, secret());
|
||||||
|
|
||||||
if (typeof validation === 'object') {
|
if (typeof validation === 'object') {
|
||||||
return !Object.keys(validation).find(key => result[key] !== validation[key]);
|
return !Object.keys(validation).find(key => result[key] !== validation[key]);
|
||||||
|
|
|
@ -0,0 +1,201 @@
|
||||||
|
import { ClickHouse } from 'clickhouse';
|
||||||
|
import dateFormat from 'dateformat';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { FILTER_IGNORED } from 'lib/constants';
|
||||||
|
import { CLICKHOUSE } from 'lib/db';
|
||||||
|
|
||||||
|
export const CLICKHOUSE_DATE_FORMATS = {
|
||||||
|
minute: '%Y-%m-%d %H:%M:00',
|
||||||
|
hour: '%Y-%m-%d %H:00:00',
|
||||||
|
day: '%Y-%m-%d',
|
||||||
|
month: '%Y-%m-01',
|
||||||
|
year: '%Y-01-01',
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = debug('umami:clickhouse');
|
||||||
|
|
||||||
|
function getClient() {
|
||||||
|
const {
|
||||||
|
hostname,
|
||||||
|
port,
|
||||||
|
pathname,
|
||||||
|
username = 'default',
|
||||||
|
password,
|
||||||
|
} = new URL(process.env.CLICKHOUSE_URL);
|
||||||
|
|
||||||
|
const client = new ClickHouse({
|
||||||
|
url: hostname,
|
||||||
|
port: Number(port),
|
||||||
|
format: 'json',
|
||||||
|
config: {
|
||||||
|
database: pathname.replace('/', ''),
|
||||||
|
},
|
||||||
|
basicAuth: password ? { username, password } : null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
global[CLICKHOUSE] = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Clickhouse initialized');
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateStringQuery(data, unit) {
|
||||||
|
return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateQuery(field, unit, timezone) {
|
||||||
|
if (timezone) {
|
||||||
|
return `date_trunc('${unit}', ${field}, '${timezone}')`;
|
||||||
|
}
|
||||||
|
return `date_trunc('${unit}', ${field})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateFormat(date) {
|
||||||
|
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBetweenDates(field, start_at, end_at) {
|
||||||
|
return `${field} between ${getDateFormat(start_at)}
|
||||||
|
and ${getDateFormat(end_at)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterQuery(table, column, filters = {}, params = []) {
|
||||||
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
|
const filter = filters[key];
|
||||||
|
|
||||||
|
if (filter === undefined || filter === FILTER_IGNORED) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'url':
|
||||||
|
if (table === 'pageview' || table === 'event') {
|
||||||
|
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||||
|
params.push(decodeURIComponent(filter));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'os':
|
||||||
|
case 'browser':
|
||||||
|
case 'device':
|
||||||
|
case 'country':
|
||||||
|
if (table === 'session') {
|
||||||
|
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||||
|
params.push(decodeURIComponent(filter));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'event_name':
|
||||||
|
if (table === 'event') {
|
||||||
|
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||||
|
params.push(decodeURIComponent(filter));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'referrer':
|
||||||
|
if (table === 'pageview' || table === 'event') {
|
||||||
|
arr.push(`and ${table}.referrer like $${params.length + 1}`);
|
||||||
|
params.push(`%${decodeURIComponent(filter)}%`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'domain':
|
||||||
|
if (table === 'pageview') {
|
||||||
|
arr.push(`and ${table}.referrer not like $${params.length + 1}`);
|
||||||
|
arr.push(`and ${table}.referrer not like '/%'`);
|
||||||
|
params.push(`%://${filter}/%`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'query':
|
||||||
|
if (table === 'pageview') {
|
||||||
|
arr.push(`and ${table}.url like '%?%'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') {
|
||||||
|
const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } =
|
||||||
|
filters;
|
||||||
|
|
||||||
|
const pageviewFilters = { domain, url, referrer, query };
|
||||||
|
const sessionFilters = { os, browser, device, country };
|
||||||
|
const eventFilters = { url: event_url, event_name };
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageviewFilters,
|
||||||
|
sessionFilters,
|
||||||
|
eventFilters,
|
||||||
|
event: { event_name },
|
||||||
|
joinSession:
|
||||||
|
os || browser || device || country
|
||||||
|
? `inner join session on ${table}.${sessionKey} = session.${sessionKey}`
|
||||||
|
: '',
|
||||||
|
pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params),
|
||||||
|
sessionQuery: getFilterQuery('session', column, sessionFilters, params),
|
||||||
|
eventQuery: getFilterQuery('event', column, eventFilters, params),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatQuery(str, params = []) {
|
||||||
|
let formattedString = str;
|
||||||
|
|
||||||
|
params.forEach((param, i) => {
|
||||||
|
let replace = param;
|
||||||
|
|
||||||
|
if (typeof param === 'string' || param instanceof String) {
|
||||||
|
replace = `'${replace}'`;
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedString = formattedString.replace(`$${i + 1}`, replace);
|
||||||
|
});
|
||||||
|
|
||||||
|
return formattedString;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rawQuery(query, params = []) {
|
||||||
|
let formattedQuery = formatQuery(query, params);
|
||||||
|
|
||||||
|
if (process.env.LOG_QUERY) {
|
||||||
|
log(formattedQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
return clickhouse.query(formattedQuery).toPromise();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findUnique(data) {
|
||||||
|
if (data.length > 1) {
|
||||||
|
throw `${data.length} records found when expecting 1.`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findFirst(data) {
|
||||||
|
return data[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
const clickhouse = process.env.CLICKHOUSE_URL && (global[CLICKHOUSE] || getClient());
|
||||||
|
|
||||||
|
export default {
|
||||||
|
client: clickhouse,
|
||||||
|
log,
|
||||||
|
getDateStringQuery,
|
||||||
|
getDateQuery,
|
||||||
|
getDateFormat,
|
||||||
|
getBetweenDates,
|
||||||
|
getFilterQuery,
|
||||||
|
parseFilters,
|
||||||
|
findUnique,
|
||||||
|
findFirst,
|
||||||
|
rawQuery,
|
||||||
|
};
|
|
@ -67,35 +67,6 @@ export const EVENT_COLORS = [
|
||||||
'#ffec16',
|
'#ffec16',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const RELATIONAL = 'relational';
|
|
||||||
export const POSTGRESQL = 'postgresql';
|
|
||||||
export const MYSQL = 'mysql';
|
|
||||||
export const CLICKHOUSE = 'clickhouse';
|
|
||||||
|
|
||||||
export const MYSQL_DATE_FORMATS = {
|
|
||||||
minute: '%Y-%m-%d %H:%i:00',
|
|
||||||
hour: '%Y-%m-%d %H:00:00',
|
|
||||||
day: '%Y-%m-%d',
|
|
||||||
month: '%Y-%m-01',
|
|
||||||
year: '%Y-01-01',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const POSTGRESQL_DATE_FORMATS = {
|
|
||||||
minute: 'YYYY-MM-DD HH24:MI:00',
|
|
||||||
hour: 'YYYY-MM-DD HH24:00:00',
|
|
||||||
day: 'YYYY-MM-DD',
|
|
||||||
month: 'YYYY-MM-01',
|
|
||||||
year: 'YYYY-01-01',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const CLICKHOUSE_DATE_FORMATS = {
|
|
||||||
minute: '%Y-%m-%d %H:%M:00',
|
|
||||||
hour: '%Y-%m-%d %H:00:00',
|
|
||||||
day: '%Y-%m-%d',
|
|
||||||
month: '%Y-%m-01',
|
|
||||||
year: '%Y-01-01',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const FILTER_IGNORED = Symbol.for('filter-ignored');
|
export const FILTER_IGNORED = Symbol.for('filter-ignored');
|
||||||
|
|
||||||
export const DOMAIN_REGEX =
|
export const DOMAIN_REGEX =
|
||||||
|
@ -106,6 +77,7 @@ export const LAPTOP_SCREEN_WIDTH = 1024;
|
||||||
export const MOBILE_SCREEN_WIDTH = 479;
|
export const MOBILE_SCREEN_WIDTH = 479;
|
||||||
|
|
||||||
export const URL_LENGTH = 500;
|
export const URL_LENGTH = 500;
|
||||||
|
export const EVENT_NAME_LENGTH = 50;
|
||||||
|
|
||||||
export const DESKTOP_OS = [
|
export const DESKTOP_OS = [
|
||||||
'Windows 3.11',
|
'Windows 3.11',
|
||||||
|
|
|
@ -1,74 +1,19 @@
|
||||||
import crypto from 'crypto';
|
import { v4, v5 } from 'uuid';
|
||||||
import { v4, v5, validate } from 'uuid';
|
|
||||||
import bcrypt from 'bcryptjs';
|
|
||||||
import { JWT, JWE, JWK } from 'jose';
|
|
||||||
import { startOfMonth } from 'date-fns';
|
import { startOfMonth } from 'date-fns';
|
||||||
|
import { hash } from 'next-basics';
|
||||||
const SALT_ROUNDS = 10;
|
|
||||||
const KEY = JWK.asKey(Buffer.from(secret()));
|
|
||||||
const ROTATING_SALT = hash(startOfMonth(new Date()).toUTCString());
|
|
||||||
const CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
||||||
|
|
||||||
export function hash(...args) {
|
|
||||||
return crypto.createHash('sha512').update(args.join('')).digest('hex');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function secret() {
|
export function secret() {
|
||||||
return hash(process.env.HASH_SALT || process.env.DATABASE_URL);
|
return hash(process.env.HASH_SALT || process.env.DATABASE_URL);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function salt() {
|
export function salt() {
|
||||||
return v5(hash(secret(), ROTATING_SALT), v5.DNS);
|
const ROTATING_SALT = hash(startOfMonth(new Date()).toUTCString());
|
||||||
|
|
||||||
|
return hash([secret(), ROTATING_SALT]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function uuid(...args) {
|
export function uuid(...args) {
|
||||||
if (!args.length) return v4();
|
if (!args.length) return v4();
|
||||||
|
|
||||||
return v5(args.join(''), salt());
|
return v5(hash([...args, salt()]), v5.DNS);
|
||||||
}
|
|
||||||
|
|
||||||
export function isValidUuid(s) {
|
|
||||||
return validate(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRandomChars(n) {
|
|
||||||
let s = '';
|
|
||||||
for (let i = 0; i < n; i++) {
|
|
||||||
s += CHARS[Math.floor(Math.random() * CHARS.length)];
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function hashPassword(password) {
|
|
||||||
return bcrypt.hashSync(password, SALT_ROUNDS);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function checkPassword(password, hash) {
|
|
||||||
return bcrypt.compareSync(password, hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createToken(payload) {
|
|
||||||
return JWT.sign(payload, 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 parseSecureToken(token) {
|
|
||||||
try {
|
|
||||||
const result = await JWE.decrypt(token, KEY);
|
|
||||||
|
|
||||||
return parseToken(result.toString());
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
37
lib/date.js
37
lib/date.js
|
@ -7,6 +7,8 @@ import {
|
||||||
addYears,
|
addYears,
|
||||||
subHours,
|
subHours,
|
||||||
subDays,
|
subDays,
|
||||||
|
subMonths,
|
||||||
|
subYears,
|
||||||
startOfMinute,
|
startOfMinute,
|
||||||
startOfHour,
|
startOfHour,
|
||||||
startOfDay,
|
startOfDay,
|
||||||
|
@ -39,7 +41,7 @@ export function getDateRange(value, locale = 'en-US') {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const dateLocale = getDateLocale(locale);
|
const dateLocale = getDateLocale(locale);
|
||||||
|
|
||||||
const match = value.match(/^(?<num>[0-9]+)(?<unit>hour|day|week|month|year)$/);
|
const match = value.match(/^(?<num>[0-9-]+)(?<unit>hour|day|week|month|year)$/);
|
||||||
|
|
||||||
if (!match) return;
|
if (!match) return;
|
||||||
|
|
||||||
|
@ -78,6 +80,39 @@ export function getDateRange(value, locale = 'en-US') {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (+num === -1) {
|
||||||
|
switch (unit) {
|
||||||
|
case 'day':
|
||||||
|
return {
|
||||||
|
startDate: subDays(startOfDay(now), 1),
|
||||||
|
endDate: subDays(endOfDay(now), 1),
|
||||||
|
unit: 'hour',
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
case 'week':
|
||||||
|
return {
|
||||||
|
startDate: subDays(startOfWeek(now, { locale: dateLocale }), 7),
|
||||||
|
endDate: subDays(endOfWeek(now, { locale: dateLocale }), 1),
|
||||||
|
unit: 'day',
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
case 'month':
|
||||||
|
return {
|
||||||
|
startDate: subMonths(startOfMonth(now), 1),
|
||||||
|
endDate: subMonths(endOfMonth(now), 1),
|
||||||
|
unit: 'day',
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
case 'year':
|
||||||
|
return {
|
||||||
|
startDate: subYears(startOfYear(now), 1),
|
||||||
|
endDate: subYears(endOfYear(now), 1),
|
||||||
|
unit: 'month',
|
||||||
|
value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch (unit) {
|
switch (unit) {
|
||||||
case 'day':
|
case 'day':
|
||||||
return {
|
return {
|
||||||
|
|
301
lib/db.js
301
lib/db.js
|
@ -1,83 +1,18 @@
|
||||||
import { PrismaClient } from '@prisma/client';
|
export const PRISMA = 'prisma';
|
||||||
import { ClickHouse } from 'clickhouse';
|
export const POSTGRESQL = 'postgresql';
|
||||||
import chalk from 'chalk';
|
export const MYSQL = 'mysql';
|
||||||
import {
|
export const CLICKHOUSE = 'clickhouse';
|
||||||
MYSQL,
|
export const KAFKA = 'kafka';
|
||||||
MYSQL_DATE_FORMATS,
|
export const KAFKA_PRODUCER = 'kafka-producer';
|
||||||
POSTGRESQL,
|
export const REDIS = 'redis';
|
||||||
POSTGRESQL_DATE_FORMATS,
|
|
||||||
CLICKHOUSE,
|
|
||||||
RELATIONAL,
|
|
||||||
FILTER_IGNORED,
|
|
||||||
} from 'lib/constants';
|
|
||||||
import moment from 'moment-timezone';
|
|
||||||
import { CLICKHOUSE_DATE_FORMATS } from './constants';
|
|
||||||
|
|
||||||
|
// Fixes issue with converting bigint values
|
||||||
BigInt.prototype.toJSON = function () {
|
BigInt.prototype.toJSON = function () {
|
||||||
return Number(this);
|
return Number(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
const options = {
|
export function getDatabaseType(url = process.env.DATABASE_URL) {
|
||||||
log: [
|
const type = process.env.DATABASE_TYPE || (url && url.split(':')[0]);
|
||||||
{
|
|
||||||
emit: 'event',
|
|
||||||
level: 'query',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
function logQuery(e) {
|
|
||||||
console.log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`));
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPrismaClient(options) {
|
|
||||||
const prisma = new PrismaClient(options);
|
|
||||||
|
|
||||||
if (process.env.LOG_QUERY) {
|
|
||||||
prisma.$on('query', logQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return prisma;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getClickhouseClient() {
|
|
||||||
if (!process.env.ANALYTICS_URL) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const url = new URL(process.env.ANALYTICS_URL);
|
|
||||||
const database = url.pathname.replace('/', '');
|
|
||||||
|
|
||||||
return new ClickHouse({
|
|
||||||
url: url.hostname,
|
|
||||||
port: Number(url.port),
|
|
||||||
basicAuth: url.password
|
|
||||||
? {
|
|
||||||
username: url.username || 'default',
|
|
||||||
password: url.password,
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
format: 'json',
|
|
||||||
config: {
|
|
||||||
database,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const prisma = global.prisma || getPrismaClient(options);
|
|
||||||
const clickhouse = global.clickhouse || getClickhouseClient();
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
|
||||||
global.prisma = prisma;
|
|
||||||
global.clickhouse = clickhouse;
|
|
||||||
}
|
|
||||||
|
|
||||||
export { prisma, clickhouse };
|
|
||||||
|
|
||||||
export function getDatabase() {
|
|
||||||
const type =
|
|
||||||
process.env.DATABASE_TYPE ||
|
|
||||||
(process.env.DATABASE_URL && process.env.DATABASE_URL.split(':')[0]);
|
|
||||||
|
|
||||||
if (type === 'postgres') {
|
if (type === 'postgres') {
|
||||||
return POSTGRESQL;
|
return POSTGRESQL;
|
||||||
|
@ -86,220 +21,18 @@ export function getDatabase() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAnalyticsDatabase() {
|
export async function runQuery(queries) {
|
||||||
const type =
|
const db = getDatabaseType(process.env.CLICKHOUSE_URL || process.env.DATABASE_URL);
|
||||||
process.env.ANALYTICS_TYPE ||
|
|
||||||
(process.env.ANALYTICS_URL && process.env.ANALYTICS_URL.split(':')[0]);
|
|
||||||
|
|
||||||
if (type === 'postgres') {
|
|
||||||
return POSTGRESQL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!type) {
|
|
||||||
return getDatabase();
|
|
||||||
}
|
|
||||||
|
|
||||||
return type;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateStringQueryClickhouse(data, unit) {
|
|
||||||
return `formatDateTime(${data}, '${CLICKHOUSE_DATE_FORMATS[unit]}')`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateQuery(field, unit, timezone) {
|
|
||||||
const db = getDatabase();
|
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
|
||||||
if (timezone) {
|
|
||||||
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
|
|
||||||
}
|
|
||||||
return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db === MYSQL) {
|
|
||||||
if (timezone) {
|
|
||||||
const tz = moment.tz(timezone).format('Z');
|
|
||||||
|
|
||||||
return `date_format(convert_tz(${field},'+00:00','${tz}'), '${MYSQL_DATE_FORMATS[unit]}')`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `date_format(${field}, '${MYSQL_DATE_FORMATS[unit]}')`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateQueryClickhouse(field, unit, timezone) {
|
|
||||||
if (timezone) {
|
|
||||||
return `date_trunc('${unit}', ${field},'${timezone}')`;
|
|
||||||
}
|
|
||||||
return `date_trunc('${unit}', ${field})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getDateFormatClickhouse(date) {
|
|
||||||
return `parseDateTimeBestEffort('${date.toUTCString()}')`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBetweenDatesClickhouse(field, start_at, end_at) {
|
|
||||||
return `${field} between ${getDateFormatClickhouse(start_at)}
|
|
||||||
and ${getDateFormatClickhouse(end_at)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getTimestampInterval(field) {
|
|
||||||
const db = getDatabase();
|
|
||||||
|
|
||||||
if (db === POSTGRESQL) {
|
|
||||||
return `floor(extract(epoch from max(${field}) - min(${field})))`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db === MYSQL) {
|
|
||||||
return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getFilterQuery(table, column, filters = {}, params = []) {
|
|
||||||
const query = Object.keys(filters).reduce((arr, key) => {
|
|
||||||
const filter = filters[key];
|
|
||||||
|
|
||||||
if (filter === undefined || filter === FILTER_IGNORED) {
|
|
||||||
return arr;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (key) {
|
|
||||||
case 'url':
|
|
||||||
if (table === 'pageview' || table === 'event') {
|
|
||||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
|
||||||
params.push(decodeURIComponent(filter));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'os':
|
|
||||||
case 'browser':
|
|
||||||
case 'device':
|
|
||||||
case 'country':
|
|
||||||
if (table === 'session') {
|
|
||||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
|
||||||
params.push(decodeURIComponent(filter));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'event_name':
|
|
||||||
if (table === 'event') {
|
|
||||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
|
||||||
params.push(decodeURIComponent(filter));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'referrer':
|
|
||||||
if (table === 'pageview' || table === 'event') {
|
|
||||||
arr.push(`and ${table}.referrer like $${params.length + 1}`);
|
|
||||||
params.push(`%${decodeURIComponent(filter)}%`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'domain':
|
|
||||||
if (table === 'pageview') {
|
|
||||||
arr.push(`and ${table}.referrer not like $${params.length + 1}`);
|
|
||||||
arr.push(`and ${table}.referrer not like '/%'`);
|
|
||||||
params.push(`%://${filter}/%`);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'query':
|
|
||||||
if (table === 'pageview') {
|
|
||||||
arr.push(`and ${table}.url like '%?%'`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return query.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') {
|
|
||||||
const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } =
|
|
||||||
filters;
|
|
||||||
|
|
||||||
console.log({ table, column, filters, params });
|
|
||||||
|
|
||||||
const pageviewFilters = { domain, url, referrer, query };
|
|
||||||
const sessionFilters = { os, browser, device, country };
|
|
||||||
const eventFilters = { url: event_url, event_name };
|
|
||||||
|
|
||||||
return {
|
|
||||||
pageviewFilters,
|
|
||||||
sessionFilters,
|
|
||||||
eventFilters,
|
|
||||||
event: { event_name },
|
|
||||||
joinSession:
|
|
||||||
os || browser || device || country
|
|
||||||
? `inner join session on ${table}.${sessionKey} = session.${sessionKey}`
|
|
||||||
: '',
|
|
||||||
pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params),
|
|
||||||
sessionQuery: getFilterQuery('session', column, sessionFilters, params),
|
|
||||||
eventQuery: getFilterQuery('event', column, eventFilters, params),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceQueryClickhouse(string, params = []) {
|
|
||||||
let formattedString = string;
|
|
||||||
|
|
||||||
params.forEach((a, i) => {
|
|
||||||
let replace = a;
|
|
||||||
|
|
||||||
if (typeof a === 'string' || a instanceof String) {
|
|
||||||
replace = `'${replace}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
formattedString = formattedString.replace(`$${i + 1}`, replace);
|
|
||||||
});
|
|
||||||
|
|
||||||
return formattedString;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runQuery(query) {
|
|
||||||
return query.catch(e => {
|
|
||||||
throw e;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rawQuery(query, params = []) {
|
|
||||||
const db = getDatabase();
|
|
||||||
|
|
||||||
if (db !== POSTGRESQL && db !== MYSQL) {
|
|
||||||
return Promise.reject(new Error('Unknown database.'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
|
|
||||||
|
|
||||||
return runQuery(prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]));
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function rawQueryClickhouse(query, params = [], debug = false) {
|
|
||||||
let formattedQuery = replaceQueryClickhouse(query, params);
|
|
||||||
|
|
||||||
if (debug || process.env.LOG_QUERY) {
|
|
||||||
console.log(formattedQuery);
|
|
||||||
}
|
|
||||||
|
|
||||||
return clickhouse.query(formattedQuery).toPromise();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function findUnique(data) {
|
|
||||||
if (data.length > 1) {
|
|
||||||
throw `${data.length} records found when expecting 1.`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data[0] ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runAnalyticsQuery(queries) {
|
|
||||||
const db = getAnalyticsDatabase();
|
|
||||||
|
|
||||||
if (db === POSTGRESQL || db === MYSQL) {
|
if (db === POSTGRESQL || db === MYSQL) {
|
||||||
return queries[RELATIONAL]();
|
return queries[PRISMA]();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (db === CLICKHOUSE) {
|
if (db === CLICKHOUSE) {
|
||||||
|
if (queries[KAFKA]) {
|
||||||
|
return queries[KAFKA]();
|
||||||
|
}
|
||||||
|
|
||||||
return queries[CLICKHOUSE]();
|
return queries[CLICKHOUSE]();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
import { removeWWW } from './url';
|
|
||||||
|
|
||||||
export const urlFilter = data => {
|
export const urlFilter = data => {
|
||||||
const isValidUrl = url => {
|
const isValidUrl = url => {
|
||||||
return url !== '' && url !== null && !url.startsWith('#');
|
return url !== '' && url !== null && !url.startsWith('#');
|
||||||
|
@ -49,7 +47,7 @@ export const refFilter = data => {
|
||||||
try {
|
try {
|
||||||
const url = new URL(x);
|
const url = new URL(x);
|
||||||
|
|
||||||
id = removeWWW(url.hostname) || url.href;
|
id = url.hostname.replace('www', '') || url.href;
|
||||||
} catch {
|
} catch {
|
||||||
id = '';
|
id = '';
|
||||||
}
|
}
|
||||||
|
@ -94,11 +92,7 @@ export const paramFilter = data => {
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const d = Object.keys(map).flatMap(key =>
|
return Object.keys(map).flatMap(key =>
|
||||||
Object.keys(map[key]).map(n => ({ x: `${key}=${n}`, p: key, v: n, y: map[key][n] })),
|
Object.keys(map[key]).map(n => ({ x: `${key}=${n}`, p: key, v: n, y: map[key][n] })),
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log({ map, d });
|
|
||||||
|
|
||||||
return d;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -74,7 +74,7 @@ export function stringToColor(str) {
|
||||||
let color = '#';
|
let color = '#';
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
let value = (hash >> (i * 8)) & 0xff;
|
let value = (hash >> (i * 8)) & 0xff;
|
||||||
color += ('00' + value.toString(16)).substr(-2);
|
color += ('00' + value.toString(16)).substring(-2);
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,84 @@
|
||||||
|
import { Kafka, logLevel } from 'kafkajs';
|
||||||
|
import dateFormat from 'dateformat';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { KAFKA, KAFKA_PRODUCER } from 'lib/db';
|
||||||
|
|
||||||
|
const log = debug('umami:kafka');
|
||||||
|
|
||||||
|
function getClient() {
|
||||||
|
const { username, password } = new URL(process.env.KAFKA_URL);
|
||||||
|
const brokers = process.env.KAFKA_BROKER.split(',');
|
||||||
|
|
||||||
|
const ssl =
|
||||||
|
username && password
|
||||||
|
? {
|
||||||
|
ssl: true,
|
||||||
|
sasl: {
|
||||||
|
mechanism: 'plain',
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
const client = new Kafka({
|
||||||
|
clientId: 'umami',
|
||||||
|
brokers: brokers,
|
||||||
|
connectionTimeout: 3000,
|
||||||
|
logLevel: logLevel.ERROR,
|
||||||
|
...ssl,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
global[KAFKA] = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
return client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getProducer() {
|
||||||
|
const producer = kafka.producer();
|
||||||
|
await producer.connect();
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
global[KAFKA_PRODUCER] = producer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return producer;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateFormat(date) {
|
||||||
|
return dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendMessage(params, topic) {
|
||||||
|
await producer.send({
|
||||||
|
topic,
|
||||||
|
messages: [
|
||||||
|
{
|
||||||
|
value: JSON.stringify(params),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
acks: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
let kafka;
|
||||||
|
let producer;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (global[KAFKA] || getClient());
|
||||||
|
|
||||||
|
if (kafka) {
|
||||||
|
producer = global[KAFKA_PRODUCER] || (await getProducer());
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default {
|
||||||
|
client: kafka,
|
||||||
|
producer: producer,
|
||||||
|
log,
|
||||||
|
getDateFormat,
|
||||||
|
sendMessage,
|
||||||
|
};
|
|
@ -1,19 +1,7 @@
|
||||||
|
import { createMiddleware, unauthorized, badRequest, serverError } from 'next-basics';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { getSession } from './session';
|
import { getSession } from './session';
|
||||||
import { getAuthToken } from './auth';
|
import { getAuthToken } from './auth';
|
||||||
import { unauthorized, badRequest, serverError } from './response';
|
|
||||||
|
|
||||||
export function createMiddleware(middleware) {
|
|
||||||
return (req, res) =>
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
middleware(req, res, result => {
|
|
||||||
if (result instanceof Error) {
|
|
||||||
return reject(result);
|
|
||||||
}
|
|
||||||
return resolve(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useCors = createMiddleware(cors());
|
export const useCors = createMiddleware(cors());
|
||||||
|
|
||||||
|
@ -23,7 +11,9 @@ export const useSession = createMiddleware(async (req, res, next) => {
|
||||||
try {
|
try {
|
||||||
session = await getSession(req);
|
session = await getSession(req);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
console.error(e);
|
console.error(e);
|
||||||
|
|
||||||
return serverError(res, e.message);
|
return serverError(res, e.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,199 @@
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import chalk from 'chalk';
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { PRISMA, MYSQL, POSTGRESQL, getDatabaseType } from 'lib/db';
|
||||||
|
import { FILTER_IGNORED } from 'lib/constants';
|
||||||
|
|
||||||
|
const MYSQL_DATE_FORMATS = {
|
||||||
|
minute: '%Y-%m-%d %H:%i:00',
|
||||||
|
hour: '%Y-%m-%d %H:00:00',
|
||||||
|
day: '%Y-%m-%d',
|
||||||
|
month: '%Y-%m-01',
|
||||||
|
year: '%Y-01-01',
|
||||||
|
};
|
||||||
|
|
||||||
|
const POSTGRESQL_DATE_FORMATS = {
|
||||||
|
minute: 'YYYY-MM-DD HH24:MI:00',
|
||||||
|
hour: 'YYYY-MM-DD HH24:00:00',
|
||||||
|
day: 'YYYY-MM-DD',
|
||||||
|
month: 'YYYY-MM-01',
|
||||||
|
year: 'YYYY-01-01',
|
||||||
|
};
|
||||||
|
|
||||||
|
const log = debug('umami:prisma');
|
||||||
|
|
||||||
|
const PRISMA_OPTIONS = {
|
||||||
|
log: [
|
||||||
|
{
|
||||||
|
emit: 'event',
|
||||||
|
level: 'query',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
function logQuery(e) {
|
||||||
|
log(chalk.yellow(e.params), '->', e.query, chalk.greenBright(`${e.duration}ms`));
|
||||||
|
}
|
||||||
|
|
||||||
|
function getClient(options) {
|
||||||
|
const prisma = new PrismaClient(options);
|
||||||
|
|
||||||
|
if (process.env.LOG_QUERY) {
|
||||||
|
prisma.$on('query', logQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
global[PRISMA] = prisma;
|
||||||
|
}
|
||||||
|
|
||||||
|
log('Prisma initialized');
|
||||||
|
|
||||||
|
return prisma;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDateQuery(field, unit, timezone) {
|
||||||
|
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
if (db === POSTGRESQL) {
|
||||||
|
if (timezone) {
|
||||||
|
return `to_char(date_trunc('${unit}', ${field} at time zone '${timezone}'), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
|
||||||
|
}
|
||||||
|
return `to_char(date_trunc('${unit}', ${field}), '${POSTGRESQL_DATE_FORMATS[unit]}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db === MYSQL) {
|
||||||
|
if (timezone) {
|
||||||
|
const tz = moment.tz(timezone).format('Z');
|
||||||
|
|
||||||
|
return `date_format(convert_tz(${field},'+00:00','${tz}'), '${MYSQL_DATE_FORMATS[unit]}')`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `date_format(${field}, '${MYSQL_DATE_FORMATS[unit]}')`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTimestampInterval(field) {
|
||||||
|
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
if (db === POSTGRESQL) {
|
||||||
|
return `floor(extract(epoch from max(${field}) - min(${field})))`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db === MYSQL) {
|
||||||
|
return `floor(unix_timestamp(max(${field})) - unix_timestamp(min(${field})))`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterQuery(table, column, filters = {}, params = []) {
|
||||||
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
|
const filter = filters[key];
|
||||||
|
|
||||||
|
if (filter === undefined || filter === FILTER_IGNORED) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (key) {
|
||||||
|
case 'url':
|
||||||
|
if (table === 'pageview' || table === 'event') {
|
||||||
|
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||||
|
params.push(decodeURIComponent(filter));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'os':
|
||||||
|
case 'browser':
|
||||||
|
case 'device':
|
||||||
|
case 'country':
|
||||||
|
if (table === 'session') {
|
||||||
|
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||||
|
params.push(decodeURIComponent(filter));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'event_name':
|
||||||
|
if (table === 'event') {
|
||||||
|
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||||
|
params.push(decodeURIComponent(filter));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'referrer':
|
||||||
|
if (table === 'pageview' || table === 'event') {
|
||||||
|
arr.push(`and ${table}.referrer like $${params.length + 1}`);
|
||||||
|
params.push(`%${decodeURIComponent(filter)}%`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'domain':
|
||||||
|
if (table === 'pageview') {
|
||||||
|
arr.push(`and ${table}.referrer not like $${params.length + 1}`);
|
||||||
|
arr.push(`and ${table}.referrer not like '/%'`);
|
||||||
|
params.push(`%://${filter}/%`);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'query':
|
||||||
|
if (table === 'pageview') {
|
||||||
|
arr.push(`and ${table}.url like '%?%'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') {
|
||||||
|
const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } =
|
||||||
|
filters;
|
||||||
|
|
||||||
|
const pageviewFilters = { domain, url, referrer, query };
|
||||||
|
const sessionFilters = { os, browser, device, country };
|
||||||
|
const eventFilters = { url: event_url, event_name };
|
||||||
|
|
||||||
|
return {
|
||||||
|
pageviewFilters,
|
||||||
|
sessionFilters,
|
||||||
|
eventFilters,
|
||||||
|
event: { event_name },
|
||||||
|
joinSession:
|
||||||
|
os || browser || device || country
|
||||||
|
? `inner join session on ${table}.${sessionKey} = session.${sessionKey}`
|
||||||
|
: '',
|
||||||
|
pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params),
|
||||||
|
sessionQuery: getFilterQuery('session', column, sessionFilters, params),
|
||||||
|
eventQuery: getFilterQuery('event', column, eventFilters, params),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function rawQuery(query, params = []) {
|
||||||
|
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
if (db !== POSTGRESQL && db !== MYSQL) {
|
||||||
|
return Promise.reject(new Error('Unknown database.'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const sql = db === MYSQL ? query.replace(/\$[0-9]+/g, '?') : query;
|
||||||
|
|
||||||
|
return prisma.$queryRawUnsafe.apply(prisma, [sql, ...params]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function transaction(queries) {
|
||||||
|
return prisma.$transaction(queries);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
const prisma = global[PRISMA] || getClient(PRISMA_OPTIONS);
|
||||||
|
|
||||||
|
export default {
|
||||||
|
client: prisma,
|
||||||
|
log,
|
||||||
|
getDateQuery,
|
||||||
|
getTimestampInterval,
|
||||||
|
getFilterQuery,
|
||||||
|
parseFilters,
|
||||||
|
rawQuery,
|
||||||
|
transaction,
|
||||||
|
};
|
|
@ -0,0 +1,58 @@
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
import { startOfMonth } from 'date-fns';
|
||||||
|
import debug from 'debug';
|
||||||
|
import { getSessions, getAllWebsites } from 'queries';
|
||||||
|
import { REDIS } from 'lib/db';
|
||||||
|
|
||||||
|
const log = debug('umami:redis');
|
||||||
|
const INITIALIZED = 'redis:initialized';
|
||||||
|
export const DELETED = 'deleted';
|
||||||
|
|
||||||
|
function getClient() {
|
||||||
|
if (!process.env.REDIS_URL) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const redis = new Redis(process.env.REDIS_URL);
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
|
global[REDIS] = redis;
|
||||||
|
}
|
||||||
|
|
||||||
|
return redis;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stageData() {
|
||||||
|
const sessions = await getSessions([], startOfMonth(new Date()));
|
||||||
|
const websites = await getAllWebsites();
|
||||||
|
|
||||||
|
const sessionUuids = sessions.map(a => {
|
||||||
|
return { key: `session:${a.session_uuid}`, value: 1 };
|
||||||
|
});
|
||||||
|
const websiteIds = websites.map(a => {
|
||||||
|
return { key: `website:${a.website_uuid}`, value: Number(a.website_id) };
|
||||||
|
});
|
||||||
|
|
||||||
|
await addRedis(sessionUuids);
|
||||||
|
await addRedis(websiteIds);
|
||||||
|
|
||||||
|
await redis.set(INITIALIZED, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRedis(ids) {
|
||||||
|
for (let i = 0; i < ids.length; i++) {
|
||||||
|
const { key, value } = ids[i];
|
||||||
|
await redis.set(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialization
|
||||||
|
const redis = process.env.REDIS_URL && (global[REDIS] || getClient());
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
if (redis && !(await redis.get(INITIALIZED))) {
|
||||||
|
await stageData();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
export default { client: redis, stageData, log };
|
|
@ -1,43 +0,0 @@
|
||||||
export function ok(res, data = {}) {
|
|
||||||
return json(res, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function json(res, data = {}) {
|
|
||||||
return res.status(200).json(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function send(res, data, type = 'text/plain') {
|
|
||||||
res.setHeader('Content-Type', type);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function redirect(res, url) {
|
|
||||||
res.setHeader('Location', url);
|
|
||||||
|
|
||||||
return res.status(303).end();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function badRequest(res, msg = '400 Bad Request') {
|
|
||||||
return res.status(400).end(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function unauthorized(res, msg = '401 Unauthorized') {
|
|
||||||
return res.status(401).end(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function forbidden(res, msg = '403 Forbidden') {
|
|
||||||
return res.status(403).end(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function notFound(res, msg = '404 Not Found') {
|
|
||||||
return res.status(404).end(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function methodNotAllowed(res, msg = '405 Method Not Allowed') {
|
|
||||||
res.status(405).end(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function serverError(res, msg = '500 Internal Server Error') {
|
|
||||||
res.status(500).end(msg);
|
|
||||||
}
|
|
|
@ -1,6 +1,9 @@
|
||||||
import { getWebsiteByUuid, getSessionByUuid, createSession } from 'queries';
|
import { parseToken } from 'next-basics';
|
||||||
import { getJsonBody, getClientInfo } from 'lib/request';
|
import { validate } from 'uuid';
|
||||||
import { uuid, isValidUuid, parseToken } from 'lib/crypto';
|
import { uuid } from 'lib/crypto';
|
||||||
|
import redis, { DELETED } from 'lib/redis';
|
||||||
|
import { getClientInfo, getJsonBody } from 'lib/request';
|
||||||
|
import { createSession, getSessionByUuid, getWebsiteByUuid } from 'queries';
|
||||||
|
|
||||||
export async function getSession(req) {
|
export async function getSession(req) {
|
||||||
const { payload } = getJsonBody(req);
|
const { payload } = getJsonBody(req);
|
||||||
|
@ -9,7 +12,6 @@ export async function getSession(req) {
|
||||||
throw new Error('Invalid request');
|
throw new Error('Invalid request');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { website: website_uuid, hostname, screen, language } = payload;
|
|
||||||
const cache = req.headers['x-umami-cache'];
|
const cache = req.headers['x-umami-cache'];
|
||||||
|
|
||||||
if (cache) {
|
if (cache) {
|
||||||
|
@ -20,26 +22,52 @@ export async function getSession(req) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isValidUuid(website_uuid)) {
|
const { website: website_uuid, hostname, screen, language } = payload;
|
||||||
throw new Error(`Invalid website: ${website_uuid}`);
|
|
||||||
|
if (!validate(website_uuid)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let websiteId = null;
|
||||||
|
|
||||||
|
// Check if website exists
|
||||||
|
if (redis.client) {
|
||||||
|
websiteId = await redis.client.get(`website:${website_uuid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check database if redis does not have
|
||||||
|
if (!websiteId) {
|
||||||
|
const { website_id } = await getWebsiteByUuid(website_uuid);
|
||||||
|
websiteId = website_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!websiteId || websiteId === DELETED) {
|
||||||
|
throw new Error(`Website not found: ${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 getWebsiteByUuid(website_uuid);
|
const session_uuid = uuid(websiteId, hostname, ip, userAgent);
|
||||||
|
|
||||||
if (!website) {
|
let sessionCreated = false;
|
||||||
throw new Error(`Website not found: ${website_uuid}`);
|
let sessionId = null;
|
||||||
|
let session = null;
|
||||||
|
|
||||||
|
// Check if session exists
|
||||||
|
if (redis.client) {
|
||||||
|
sessionCreated = !!(await redis.client.get(`session:${session_uuid}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { website_id } = website;
|
// Check database if redis does not have
|
||||||
const session_uuid = uuid(website_id, hostname, ip, userAgent);
|
if (!sessionCreated) {
|
||||||
|
session = await getSessionByUuid(session_uuid);
|
||||||
|
sessionCreated = !!session;
|
||||||
|
sessionId = session ? session.session_id : null;
|
||||||
|
}
|
||||||
|
|
||||||
let session = await getSessionByUuid(session_uuid);
|
if (!sessionCreated) {
|
||||||
|
|
||||||
if (!session) {
|
|
||||||
try {
|
try {
|
||||||
session = await createSession(website_id, {
|
session = await createSession(websiteId, {
|
||||||
session_uuid,
|
session_uuid,
|
||||||
hostname,
|
hostname,
|
||||||
browser,
|
browser,
|
||||||
|
@ -50,21 +78,17 @@ export async function getSession(req) {
|
||||||
device,
|
device,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!session) {
|
sessionId = session ? session.session_id : null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!e.message.includes('Unique constraint')) {
|
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { session_id } = session;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
website_id,
|
website_id: websiteId,
|
||||||
session_id,
|
session_id: sessionId,
|
||||||
session_uuid,
|
session_uuid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
35
lib/url.js
35
lib/url.js
|
@ -1,35 +0,0 @@
|
||||||
export function removeTrailingSlash(url) {
|
|
||||||
return url && url.length > 1 && url.endsWith('/') ? url.slice(0, -1) : url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeWWW(url) {
|
|
||||||
return url && url.length > 1 && url.startsWith('www.') ? url.slice(4) : url;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getQueryString(params = {}) {
|
|
||||||
const map = Object.keys(params).reduce((arr, key) => {
|
|
||||||
if (params[key] !== undefined) {
|
|
||||||
return arr.concat(`${key}=${encodeURIComponent(params[key])}`);
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (map.length) {
|
|
||||||
return `?${map.join('&')}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeUrl(url, params) {
|
|
||||||
return `${url}${getQueryString(params)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function safeDecodeURI(s) {
|
|
||||||
try {
|
|
||||||
return decodeURI(s);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
return s;
|
|
||||||
}
|
|
78
lib/web.js
78
lib/web.js
|
@ -1,78 +0,0 @@
|
||||||
import { makeUrl } from './url';
|
|
||||||
|
|
||||||
export const apiRequest = (method, url, body, headers) => {
|
|
||||||
return fetch(url, {
|
|
||||||
method,
|
|
||||||
cache: 'no-cache',
|
|
||||||
credentials: 'same-origin',
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/json',
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
...headers,
|
|
||||||
},
|
|
||||||
body,
|
|
||||||
}).then(res => {
|
|
||||||
if (res.ok) {
|
|
||||||
return res.json().then(data => ({ ok: res.ok, status: res.status, data }));
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.text().then(data => ({ ok: res.ok, status: res.status, res: res, data }));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const get = (url, params, headers) =>
|
|
||||||
apiRequest('get', makeUrl(url, params), undefined, headers);
|
|
||||||
|
|
||||||
export const del = (url, params, headers) =>
|
|
||||||
apiRequest('delete', makeUrl(url, params), undefined, headers);
|
|
||||||
|
|
||||||
export const post = (url, params, headers) =>
|
|
||||||
apiRequest('post', url, JSON.stringify(params), headers);
|
|
||||||
|
|
||||||
export const put = (url, params, headers) =>
|
|
||||||
apiRequest('put', url, JSON.stringify(params), headers);
|
|
||||||
|
|
||||||
export const hook = (_this, method, callback) => {
|
|
||||||
const orig = _this[method];
|
|
||||||
|
|
||||||
return (...args) => {
|
|
||||||
callback.apply(null, args);
|
|
||||||
|
|
||||||
return orig.apply(_this, args);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const doNotTrack = () => {
|
|
||||||
const { doNotTrack, navigator, external } = window;
|
|
||||||
|
|
||||||
const msTrackProtection = 'msTrackingProtectionEnabled';
|
|
||||||
const msTracking = () => {
|
|
||||||
return external && msTrackProtection in external && external[msTrackProtection]();
|
|
||||||
};
|
|
||||||
|
|
||||||
const dnt = doNotTrack || navigator.doNotTrack || navigator.msDoNotTrack || msTracking();
|
|
||||||
|
|
||||||
return dnt == '1' || dnt === 'yes';
|
|
||||||
};
|
|
||||||
|
|
||||||
export const setItem = (key, data, session) => {
|
|
||||||
if (typeof window !== 'undefined' && data) {
|
|
||||||
(session ? sessionStorage : localStorage).setItem(key, JSON.stringify(data));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getItem = (key, session) => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
const value = (session ? sessionStorage : localStorage).getItem(key);
|
|
||||||
|
|
||||||
if (value !== 'undefined') {
|
|
||||||
return JSON.parse(value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const removeItem = (key, session) => {
|
|
||||||
if (typeof window !== 'undefined') {
|
|
||||||
(session ? sessionStorage : localStorage).removeItem(key);
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,2 +1,2 @@
|
||||||
[functions]
|
[functions]
|
||||||
included_files = ["public/geo/*.mmdb"]
|
included_files = ["node_modules/.geo/**"]
|
||||||
|
|
24
package.json
24
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "1.37.0",
|
"version": "1.38.0-beta.1",
|
||||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||||
"author": "Mike Cao <mike@mikecao.com>",
|
"author": "Mike Cao <mike@mikecao.com>",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
@ -11,8 +11,9 @@
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "npm-run-all build-tracker build-geo build-db build-app",
|
"build": "npm-run-all build-db check-db build-tracker build-geo build-app",
|
||||||
"start": "npm-run-all check-db start-next",
|
"start": "start-next",
|
||||||
|
"build-docker": "npm-run-all build-db build-tracker build-geo build-app",
|
||||||
"start-docker": "npm-run-all check-db update-tracker start-server",
|
"start-docker": "npm-run-all check-db update-tracker start-server",
|
||||||
"start-env": "node scripts/start-env.js",
|
"start-env": "node scripts/start-env.js",
|
||||||
"start-server": "node server.js",
|
"start-server": "node server.js",
|
||||||
|
@ -56,8 +57,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "4.5.7",
|
"@fontsource/inter": "4.5.7",
|
||||||
"@prisma/client": "4.1.1",
|
"@prisma/client": "4.2.1",
|
||||||
"bcryptjs": "^2.4.3",
|
|
||||||
"chalk": "^4.1.1",
|
"chalk": "^4.1.1",
|
||||||
"chart.js": "^2.9.4",
|
"chart.js": "^2.9.4",
|
||||||
"classnames": "^2.3.1",
|
"classnames": "^2.3.1",
|
||||||
|
@ -67,21 +67,25 @@
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"date-fns": "^2.23.0",
|
"date-fns": "^2.23.0",
|
||||||
"date-fns-tz": "^1.1.4",
|
"date-fns-tz": "^1.1.4",
|
||||||
|
"dateformat": "^5.0.3",
|
||||||
|
"debug": "^4.3.4",
|
||||||
"del": "^6.0.0",
|
"del": "^6.0.0",
|
||||||
"detect-browser": "^5.2.0",
|
"detect-browser": "^5.2.0",
|
||||||
"dotenv": "^10.0.0",
|
"dotenv": "^10.0.0",
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"fs-extra": "^10.0.1",
|
"fs-extra": "^10.0.1",
|
||||||
"immer": "^9.0.12",
|
"immer": "^9.0.12",
|
||||||
|
"ioredis": "^5.2.3",
|
||||||
"ipaddr.js": "^2.0.1",
|
"ipaddr.js": "^2.0.1",
|
||||||
"is-ci": "^3.0.1",
|
"is-ci": "^3.0.1",
|
||||||
"is-docker": "^3.0.0",
|
"is-docker": "^3.0.0",
|
||||||
"is-localhost-ip": "^1.4.0",
|
"is-localhost-ip": "^1.4.0",
|
||||||
"isbot": "^3.4.5",
|
"isbot": "^3.4.5",
|
||||||
"jose": "2.0.5",
|
"kafkajs": "^2.1.0",
|
||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
"moment-timezone": "^0.5.33",
|
"moment-timezone": "^0.5.33",
|
||||||
"next": "^12.2.4",
|
"next": "^12.2.5",
|
||||||
|
"next-basics": "^0.6.0",
|
||||||
"node-fetch": "^3.2.8",
|
"node-fetch": "^3.2.8",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prop-types": "^15.7.2",
|
"prop-types": "^15.7.2",
|
||||||
|
@ -94,7 +98,7 @@
|
||||||
"react-tooltip": "^4.2.21",
|
"react-tooltip": "^4.2.21",
|
||||||
"react-use-measure": "^2.0.4",
|
"react-use-measure": "^2.0.4",
|
||||||
"react-window": "^1.8.6",
|
"react-window": "^1.8.6",
|
||||||
"request-ip": "^2.1.3",
|
"request-ip": "^3.3.0",
|
||||||
"semver": "^7.3.6",
|
"semver": "^7.3.6",
|
||||||
"thenby": "^1.3.4",
|
"thenby": "^1.3.4",
|
||||||
"timezone-support": "^2.0.2",
|
"timezone-support": "^2.0.2",
|
||||||
|
@ -110,6 +114,8 @@
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-config-next": "^12.2.4",
|
"eslint-config-next": "^12.2.4",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
"eslint-config-prettier": "^8.5.0",
|
||||||
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
|
"eslint-plugin-import": "^2.26.0",
|
||||||
"eslint-plugin-prettier": "^4.0.0",
|
"eslint-plugin-prettier": "^4.0.0",
|
||||||
"extract-react-intl-messages": "^4.1.1",
|
"extract-react-intl-messages": "^4.1.1",
|
||||||
"husky": "^7.0.0",
|
"husky": "^7.0.0",
|
||||||
|
@ -120,7 +126,7 @@
|
||||||
"postcss-preset-env": "7.4.3",
|
"postcss-preset-env": "7.4.3",
|
||||||
"postcss-rtlcss": "^3.6.1",
|
"postcss-rtlcss": "^3.6.1",
|
||||||
"prettier": "^2.6.2",
|
"prettier": "^2.6.2",
|
||||||
"prisma": "4.1.1",
|
"prisma": "4.2.1",
|
||||||
"prompts": "2.4.2",
|
"prompts": "2.4.2",
|
||||||
"rollup": "^2.70.1",
|
"rollup": "^2.70.1",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getAccountById, deleteAccount } from 'queries';
|
import { getAccountById, deleteAccount } from 'queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
|
import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics';
|
||||||
import { getAccountById, getAccountByUsername, updateAccount, createAccount } from 'queries';
|
import { getAccountById, getAccountByUsername, updateAccount, createAccount } from 'queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { hashPassword } from 'lib/crypto';
|
|
||||||
import { ok, unauthorized, methodNotAllowed, badRequest } from 'lib/response';
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
|
@ -1,7 +1,13 @@
|
||||||
import { getAccountById, updateAccount } from 'queries';
|
import { getAccountById, updateAccount } from 'queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { badRequest, methodNotAllowed, ok, unauthorized } from 'lib/response';
|
import {
|
||||||
import { checkPassword, hashPassword } from 'lib/crypto';
|
badRequest,
|
||||||
|
methodNotAllowed,
|
||||||
|
ok,
|
||||||
|
unauthorized,
|
||||||
|
checkPassword,
|
||||||
|
hashPassword,
|
||||||
|
} from 'next-basics';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getAccounts } from 'queries';
|
import { getAccounts } from 'queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
|
import { ok, unauthorized, methodNotAllowed } from 'next-basics';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { checkPassword, createSecureToken } from 'lib/crypto';
|
import { ok, unauthorized, badRequest, checkPassword, createSecureToken } from 'next-basics';
|
||||||
import { getAccountByUsername } from 'queries/admin/account/getAccountByUsername';
|
import { getAccountByUsername } from 'queries/admin/account/getAccountByUsername';
|
||||||
import { ok, unauthorized, badRequest } from 'lib/response';
|
import { secret } from 'lib/crypto';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { username, password } = req.body;
|
const { username, password } = req.body;
|
||||||
|
@ -11,10 +11,10 @@ export default async (req, res) => {
|
||||||
|
|
||||||
const account = await getAccountByUsername(username);
|
const account = await getAccountByUsername(username);
|
||||||
|
|
||||||
if (account && (await checkPassword(password, account.password))) {
|
if (account && checkPassword(password, account.password)) {
|
||||||
const { user_id, username, is_admin } = account;
|
const { user_id, username, is_admin } = account;
|
||||||
const user = { user_id, username, is_admin };
|
const user = { user_id, username, is_admin };
|
||||||
const token = await createSecureToken(user);
|
const token = createSecureToken(user, secret());
|
||||||
|
|
||||||
return ok(res, { token, user });
|
return ok(res, { token, user });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, unauthorized } from 'lib/response';
|
import { ok, unauthorized } from 'next-basics';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
const { Resolver } = require('dns').promises;
|
const { Resolver } = require('dns').promises;
|
||||||
import isbot from 'isbot';
|
import isbot from 'isbot';
|
||||||
import ipaddr from 'ipaddr.js';
|
import ipaddr from 'ipaddr.js';
|
||||||
|
import { createToken, unauthorized, send, badRequest, forbidden } from 'next-basics';
|
||||||
import { savePageView, saveEvent } from 'queries';
|
import { savePageView, saveEvent } from 'queries';
|
||||||
import { useCors, useSession } from 'lib/middleware';
|
import { useCors, useSession } from 'lib/middleware';
|
||||||
import { getJsonBody, getIpAddress } from 'lib/request';
|
import { getJsonBody, getIpAddress } from 'lib/request';
|
||||||
import { ok, send, badRequest, forbidden } from 'lib/response';
|
import { secret, uuid } from 'lib/crypto';
|
||||||
import { createToken } from 'lib/crypto';
|
|
||||||
import { removeTrailingSlash } from 'lib/url';
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
|
|
||||||
if (isbot(req.headers['user-agent'])) {
|
if (isbot(req.headers['user-agent']) && !process.env.DISABLE_BOT_CHECK) {
|
||||||
return ok(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const ignoreIps = process.env.IGNORE_IP;
|
const ignoreIps = process.env.IGNORE_IP;
|
||||||
|
@ -68,18 +67,27 @@ export default async (req, res) => {
|
||||||
let { url, referrer, event_name, event_data } = payload;
|
let { url, referrer, event_name, event_data } = payload;
|
||||||
|
|
||||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||||
url = removeTrailingSlash(url);
|
url = url.replace(/\/$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const event_uuid = uuid();
|
||||||
|
|
||||||
if (type === 'pageview') {
|
if (type === 'pageview') {
|
||||||
await savePageView(website_id, { session_id, session_uuid, url, referrer });
|
await savePageView(website_id, { session_id, session_uuid, url, referrer });
|
||||||
} else if (type === 'event') {
|
} else if (type === 'event') {
|
||||||
await saveEvent(website_id, { session_id, session_uuid, url, event_name, event_data });
|
await saveEvent(website_id, {
|
||||||
|
event_uuid,
|
||||||
|
session_id,
|
||||||
|
session_uuid,
|
||||||
|
url,
|
||||||
|
event_name,
|
||||||
|
event_data,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
return badRequest(res);
|
return badRequest(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await createToken({ website_id, session_id });
|
const token = createToken({ website_id, session_id, session_uuid }, secret());
|
||||||
|
|
||||||
return send(res, token);
|
return send(res, token);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { ok, methodNotAllowed } from 'lib/response';
|
import { ok, methodNotAllowed } from 'next-basics';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { ok } from 'next-basics';
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
return ok(res, 'nice');
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import { subMinutes } from 'date-fns';
|
import { subMinutes } from 'date-fns';
|
||||||
|
import { ok, methodNotAllowed, createToken } from 'next-basics';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, methodNotAllowed } from 'lib/response';
|
|
||||||
import { getUserWebsites, getRealtimeData } from 'queries';
|
import { getUserWebsites, getRealtimeData } from 'queries';
|
||||||
import { createToken } from 'lib/crypto';
|
import { secret } from 'lib/crypto';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
@ -12,7 +12,7 @@ export default async (req, res) => {
|
||||||
|
|
||||||
const websites = await getUserWebsites(user_id);
|
const websites = await getUserWebsites(user_id);
|
||||||
const ids = websites.map(({ website_id }) => website_id);
|
const ids = websites.map(({ website_id }) => website_id);
|
||||||
const token = await createToken({ websites: ids });
|
const token = createToken({ websites: ids }, secret());
|
||||||
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
|
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
|
||||||
|
|
||||||
return ok(res, {
|
return ok(res, {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
|
import { ok, methodNotAllowed, badRequest, parseToken } from 'next-basics';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, methodNotAllowed, badRequest } from 'lib/response';
|
|
||||||
import { getRealtimeData } from 'queries';
|
import { getRealtimeData } from 'queries';
|
||||||
import { parseToken } from 'lib/crypto';
|
|
||||||
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||||
|
import { secret } from 'lib/crypto';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
@ -16,7 +16,7 @@ export default async (req, res) => {
|
||||||
return badRequest(res);
|
return badRequest(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websites } = await parseToken(token);
|
const { websites } = parseToken(token, secret());
|
||||||
|
|
||||||
const data = await getRealtimeData(websites, new Date(+start_at));
|
const data = await getRealtimeData(websites, new Date(+start_at));
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getWebsiteByShareId } from 'queries';
|
import { getWebsiteByShareId } from 'queries';
|
||||||
import { ok, notFound, methodNotAllowed } from 'lib/response';
|
import { ok, notFound, methodNotAllowed, createToken } from 'next-basics';
|
||||||
import { createToken } from 'lib/crypto';
|
import { secret } from 'lib/crypto';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id } = req.query;
|
const { id } = req.query;
|
||||||
|
@ -10,7 +10,7 @@ export default async (req, res) => {
|
||||||
|
|
||||||
if (website) {
|
if (website) {
|
||||||
const websiteId = website.website_id;
|
const websiteId = website.website_id;
|
||||||
const token = await createToken({ website_id: websiteId });
|
const token = createToken({ website_id: websiteId }, secret());
|
||||||
|
|
||||||
return ok(res, { websiteId, token });
|
return ok(res, { websiteId, token });
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useCors } from 'lib/middleware';
|
||||||
import { getActiveVisitors } from 'queries';
|
import { getActiveVisitors } from 'queries';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { getEventMetrics } from 'queries';
|
import { getEventMetrics } from 'queries';
|
||||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
|
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useCors } from 'lib/middleware';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { deleteWebsite, getWebsiteById } from 'queries';
|
import { deleteWebsite, getWebsiteById } from 'queries';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useCors } from 'lib/middleware';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'queries';
|
import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'queries';
|
||||||
import { ok, methodNotAllowed, unauthorized, badRequest } from 'lib/response';
|
import { ok, methodNotAllowed, unauthorized, badRequest } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useCors } from 'lib/middleware';
|
||||||
import { FILTER_IGNORED } from 'lib/constants';
|
import { FILTER_IGNORED } from 'lib/constants';
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { getPageviewStats } from 'queries';
|
import { getPageviewStats } from 'queries';
|
||||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'lib/response';
|
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useCors } from 'lib/middleware';
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { resetWebsite } from 'queries';
|
import { resetWebsite } from 'queries';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { getWebsiteStats } from 'queries';
|
import { getWebsiteStats } from 'queries';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useCors } from 'lib/middleware';
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
|
import { ok, unauthorized, methodNotAllowed, getRandomChars } from 'next-basics';
|
||||||
import { updateWebsite, createWebsite, getWebsiteById } from 'queries';
|
import { updateWebsite, createWebsite, getWebsiteById } from 'queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { uuid, getRandomChars } from 'lib/crypto';
|
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);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { getAllWebsites, getUserWebsites } from 'queries';
|
import { getAllWebsites, getUserWebsites } from 'queries';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, methodNotAllowed, unauthorized } from 'lib/response';
|
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { removeItem } from 'lib/web';
|
import { removeItem } from 'next-basics';
|
||||||
import { AUTH_TOKEN } from 'lib/constants';
|
import { AUTH_TOKEN } from 'lib/constants';
|
||||||
import { setUser } from 'store/app';
|
import { setUser } from 'store/app';
|
||||||
|
|
||||||
|
|
|
@ -397,6 +397,12 @@
|
||||||
"value": "Llocs web"
|
"value": "Llocs web"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Ahir"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
|
@ -397,6 +397,12 @@
|
||||||
"value": "Webseiten"
|
"value": "Webseiten"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Gestern"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
@ -474,7 +480,7 @@
|
||||||
"message.edit-dashboard": [
|
"message.edit-dashboard": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit dashboard"
|
"value": "Dashboard bearbeiten"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.failure": [
|
"message.failure": [
|
||||||
|
@ -770,7 +776,7 @@
|
||||||
"metrics.query-parameters": [
|
"metrics.query-parameters": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Query parameters"
|
"value": "Abfrageparameter"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.referrers": [
|
"metrics.referrers": [
|
||||||
|
|
|
@ -397,6 +397,12 @@
|
||||||
"value": "Websites"
|
"value": "Websites"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Yesterday"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
|
@ -397,6 +397,12 @@
|
||||||
"value": "Websites"
|
"value": "Websites"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Yesterday"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
|
@ -397,6 +397,12 @@
|
||||||
"value": "Sitios"
|
"value": "Sitios"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Ayer"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
|
@ -38,7 +38,7 @@
|
||||||
"label.all-websites": [
|
"label.all-websites": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Tous les sites web"
|
"value": "Tous les sites"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.back": [
|
"label.back": [
|
||||||
|
@ -170,7 +170,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Langage"
|
"value": "Langue"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
@ -332,7 +332,7 @@
|
||||||
"label.this-month": [
|
"label.this-month": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Ce mois ci"
|
"value": "Ce mois"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.this-week": [
|
"label.this-week": [
|
||||||
|
@ -389,6 +389,12 @@
|
||||||
"value": "Sites"
|
"value": "Sites"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Hier"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
@ -444,7 +450,7 @@
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Êtes-vous sûr de vouloir réinistialiser les statistiques de "
|
"value": "Êtes-vous sûr de vouloir réinitialiser les statistiques de "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
@ -470,7 +476,7 @@
|
||||||
"message.edit-dashboard": [
|
"message.edit-dashboard": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit dashboard"
|
"value": "Modifier le tableau de bord"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.failure": [
|
"message.failure": [
|
||||||
|
@ -560,7 +566,7 @@
|
||||||
"message.no-websites-configured": [
|
"message.no-websites-configured": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Vous n'avez configuré aucun site Web."
|
"value": "Vous n'avez configuré aucun site."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.page-not-found": [
|
"message.page-not-found": [
|
||||||
|
@ -582,7 +588,7 @@
|
||||||
"message.reset-warning": [
|
"message.reset-warning": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Toutes les statistiques pour ce site seront supprimés, mais votre code de suivi restera intact."
|
"value": "Toutes les statistiques pour ce site seront supprimées, mais votre code de suivi restera intact."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.save-success": [
|
"message.save-success": [
|
||||||
|
@ -630,7 +636,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": " de votre site Web."
|
"value": " de votre site."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.type-delete": [
|
"message.type-delete": [
|
||||||
|
@ -742,7 +748,7 @@
|
||||||
"metrics.languages": [
|
"metrics.languages": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Langages"
|
"value": "Langues"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.operating-systems": [
|
"metrics.operating-systems": [
|
||||||
|
@ -766,7 +772,7 @@
|
||||||
"metrics.query-parameters": [
|
"metrics.query-parameters": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Query parameters"
|
"value": "Paramètres d'URL"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.referrers": [
|
"metrics.referrers": [
|
||||||
|
@ -778,7 +784,7 @@
|
||||||
"metrics.screens": [
|
"metrics.screens": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Tailles d'écran"
|
"value": "Résolutions d'écran"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.unique-visitors": [
|
"metrics.unique-visitors": [
|
||||||
|
|
|
@ -397,6 +397,12 @@
|
||||||
"value": "Siti web"
|
"value": "Siti web"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Ieri"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
|
|
@ -454,7 +454,7 @@
|
||||||
"message.edit-dashboard": [
|
"message.edit-dashboard": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit dashboard"
|
"value": "编辑仪表板"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.failure": [
|
"message.failure": [
|
||||||
|
@ -758,7 +758,7 @@
|
||||||
"metrics.query-parameters": [
|
"metrics.query-parameters": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Query parameters"
|
"value": "查询参数"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.referrers": [
|
"metrics.referrers": [
|
||||||
|
|
|
@ -170,7 +170,7 @@
|
||||||
"label.language": [
|
"label.language": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Language"
|
"value": "語言"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.last-days": [
|
"label.last-days": [
|
||||||
|
@ -454,7 +454,7 @@
|
||||||
"message.edit-dashboard": [
|
"message.edit-dashboard": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit dashboard"
|
"value": "編輯管理面板"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.failure": [
|
"message.failure": [
|
||||||
|
@ -754,7 +754,7 @@
|
||||||
"metrics.query-parameters": [
|
"metrics.query-parameters": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Query parameters"
|
"value": "查詢參數"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.referrers": [
|
"metrics.referrers": [
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
import { prisma, runQuery } from 'lib/db';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function createAccount(data) {
|
export async function createAccount(data) {
|
||||||
return runQuery(
|
return prisma.client.account.create({
|
||||||
prisma.account.create({
|
data,
|
||||||
data,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,50 @@
|
||||||
import { prisma, runQuery } from 'lib/db';
|
import prisma from 'lib/prisma';
|
||||||
|
import redis, { DELETED } from 'lib/redis';
|
||||||
|
|
||||||
export async function deleteAccount(user_id) {
|
export async function deleteAccount(user_id) {
|
||||||
return runQuery(
|
const { client } = prisma;
|
||||||
prisma.account.delete({
|
|
||||||
where: {
|
const websites = await client.website.findMany({
|
||||||
user_id,
|
where: { user_id },
|
||||||
},
|
select: { website_uuid: true },
|
||||||
}),
|
});
|
||||||
);
|
|
||||||
|
let websiteUuids = [];
|
||||||
|
|
||||||
|
if (websites.length > 0) {
|
||||||
|
websiteUuids = websites.map(a => a.website_uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
return client
|
||||||
|
.$transaction([
|
||||||
|
client.pageview.deleteMany({
|
||||||
|
where: { session: { website: { user_id } } },
|
||||||
|
}),
|
||||||
|
client.event_data.deleteMany({
|
||||||
|
where: { event: { session: { website: { user_id } } } },
|
||||||
|
}),
|
||||||
|
client.event.deleteMany({
|
||||||
|
where: { session: { website: { user_id } } },
|
||||||
|
}),
|
||||||
|
client.session.deleteMany({
|
||||||
|
where: { website: { user_id } },
|
||||||
|
}),
|
||||||
|
client.website.deleteMany({
|
||||||
|
where: { user_id },
|
||||||
|
}),
|
||||||
|
client.account.delete({
|
||||||
|
where: {
|
||||||
|
user_id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
.then(async res => {
|
||||||
|
if (redis.client) {
|
||||||
|
for (let i = 0; i < websiteUuids.length; i++) {
|
||||||
|
await redis.client.set(`website:${websiteUuids[i]}`, DELETED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { prisma, runQuery } from 'lib/db';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function getAccountById(user_id) {
|
export async function getAccountById(user_id) {
|
||||||
return runQuery(
|
return prisma.client.account.findUnique({
|
||||||
prisma.account.findUnique({
|
where: {
|
||||||
where: {
|
user_id,
|
||||||
user_id,
|
},
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { prisma, runQuery } from 'lib/db';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function getAccountByUsername(username) {
|
export async function getAccountByUsername(username) {
|
||||||
return runQuery(
|
return prisma.client.account.findUnique({
|
||||||
prisma.account.findUnique({
|
where: {
|
||||||
where: {
|
username,
|
||||||
username,
|
},
|
||||||
},
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,19 @@
|
||||||
import { prisma, runQuery } from 'lib/db';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function getAccounts() {
|
export async function getAccounts() {
|
||||||
return runQuery(
|
return prisma.client.account.findMany({
|
||||||
prisma.account.findMany({
|
orderBy: [
|
||||||
orderBy: [
|
{ is_admin: 'desc' },
|
||||||
{ is_admin: 'desc' },
|
{
|
||||||
{
|
username: 'asc',
|
||||||
username: 'asc',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
select: {
|
|
||||||
user_id: true,
|
|
||||||
username: true,
|
|
||||||
is_admin: true,
|
|
||||||
created_at: true,
|
|
||||||
updated_at: true,
|
|
||||||
},
|
},
|
||||||
}),
|
],
|
||||||
);
|
select: {
|
||||||
|
user_id: true,
|
||||||
|
username: true,
|
||||||
|
is_admin: true,
|
||||||
|
created_at: true,
|
||||||
|
updated_at: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,10 @@
|
||||||
import { prisma, runQuery } from 'lib/db';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function updateAccount(user_id, data) {
|
export async function updateAccount(user_id, data) {
|
||||||
return runQuery(
|
return prisma.client.account.update({
|
||||||
prisma.account.update({
|
where: {
|
||||||
where: {
|
user_id,
|
||||||
user_id,
|
},
|
||||||
},
|
data,
|
||||||
data,
|
});
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import { prisma, runQuery } from 'lib/db';
|
import prisma from 'lib/prisma';
|
||||||
|
import redis from 'lib/redis';
|
||||||
|
|
||||||
export async function createWebsite(user_id, data) {
|
export async function createWebsite(user_id, data) {
|
||||||
return runQuery(
|
return prisma.client.website
|
||||||
prisma.website.create({
|
.create({
|
||||||
data: {
|
data: {
|
||||||
account: {
|
account: {
|
||||||
connect: {
|
connect: {
|
||||||
|
@ -11,6 +12,12 @@ export async function createWebsite(user_id, data) {
|
||||||
},
|
},
|
||||||
...data,
|
...data,
|
||||||
},
|
},
|
||||||
}),
|
})
|
||||||
);
|
.then(async res => {
|
||||||
|
if (redis.client && res) {
|
||||||
|
await redis.client.set(`website:${res.website_uuid}`, res.website_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue