commit
80f22313f6
|
@ -48,13 +48,7 @@ mysql://username:mypassword@localhost:3306/mydb
|
||||||
yarn build
|
yarn build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Create database tables
|
The build step will also create tables in your database if you ae installing for the first time. It will also create a login account with username **admin** and password **umami**.
|
||||||
|
|
||||||
```bash
|
|
||||||
yarn update-db
|
|
||||||
```
|
|
||||||
|
|
||||||
This will also create a login account with username **admin** and password **umami**.
|
|
||||||
|
|
||||||
### Start the application
|
### Start the application
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
import List from 'assets/list-ul.svg';
|
||||||
|
import Modal from 'components/common/Modal';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import Button from './Button';
|
||||||
|
import EventDataForm from 'components/forms/EventDataForm';
|
||||||
|
import styles from './EventDataButton.module.css';
|
||||||
|
|
||||||
|
function EventDataButton({ websiteId }) {
|
||||||
|
const [showEventData, setShowEventData] = useState(false);
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
if (!showEventData) {
|
||||||
|
setShowEventData(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClose() {
|
||||||
|
setShowEventData(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
icon={<List />}
|
||||||
|
tooltip={<FormattedMessage id="label.event-data" defaultMessage="Event" />}
|
||||||
|
tooltipId="button-event"
|
||||||
|
size="small"
|
||||||
|
onClick={handleClick}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
|
Event Data
|
||||||
|
</Button>
|
||||||
|
{showEventData && (
|
||||||
|
<Modal title={<FormattedMessage id="label.event-data" defaultMessage="Query Event Data" />}>
|
||||||
|
<EventDataForm websiteId={websiteId} onClose={handleClose} />
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
EventDataButton.propTypes = {
|
||||||
|
websiteId: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventDataButton;
|
|
@ -0,0 +1,3 @@
|
||||||
|
.button {
|
||||||
|
width: fit-content;
|
||||||
|
}
|
|
@ -11,7 +11,7 @@ import useDateRange from 'hooks/useDateRange';
|
||||||
function RefreshButton({ websiteId }) {
|
function RefreshButton({ websiteId }) {
|
||||||
const [dateRange] = useDateRange(websiteId);
|
const [dateRange] = useDateRange(websiteId);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const selector = useCallback(state => state[`/website/${websiteId}/stats`], [websiteId]);
|
const selector = useCallback(state => state[`/websites/${websiteId}/stats`], [websiteId]);
|
||||||
const completed = useStore(selector);
|
const completed = useStore(selector);
|
||||||
|
|
||||||
function handleClick() {
|
function handleClick() {
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default function UpdateNotice() {
|
||||||
function handleViewClick() {
|
function handleViewClick() {
|
||||||
updateCheck();
|
updateCheck();
|
||||||
setDismissed(true);
|
setDismissed(true);
|
||||||
location.href = releaseUrl || REPO_URL;
|
open(releaseUrl || REPO_URL, '_blank');
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDismissClick() {
|
function handleDismissClick() {
|
||||||
|
|
|
@ -15,13 +15,13 @@ const initialValues = {
|
||||||
password: '',
|
password: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
const validate = ({ user_id, username, password }) => {
|
const validate = ({ id, username, password }) => {
|
||||||
const errors = {};
|
const errors = {};
|
||||||
|
|
||||||
if (!username) {
|
if (!username) {
|
||||||
errors.username = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
errors.username = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||||
}
|
}
|
||||||
if (!user_id && !password) {
|
if (!id && !password) {
|
||||||
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +33,8 @@ export default function AccountEditForm({ values, onSave, onClose }) {
|
||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
const { ok, data } = await post('/account', values);
|
const { id } = values;
|
||||||
|
const { ok, data } = await post(id ? `/accounts/${id}` : '/accounts', values);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
onSave();
|
onSave();
|
||||||
|
|
|
@ -9,6 +9,7 @@ import FormLayout, {
|
||||||
FormRow,
|
FormRow,
|
||||||
} from 'components/layout/FormLayout';
|
} from 'components/layout/FormLayout';
|
||||||
import useApi from 'hooks/useApi';
|
import useApi from 'hooks/useApi';
|
||||||
|
import useUser from '../../hooks/useUser';
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
current_password: '',
|
current_password: '',
|
||||||
|
@ -39,9 +40,10 @@ const validate = ({ current_password, new_password, confirm_password }) => {
|
||||||
export default function ChangePasswordForm({ values, onSave, onClose }) {
|
export default function ChangePasswordForm({ values, onSave, onClose }) {
|
||||||
const { post } = useApi();
|
const { post } = useApi();
|
||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
|
const { user } = useUser();
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
const { ok, data } = await post('/account/password', values);
|
const { ok, data } = await post(`/accounts/${user.userId}/password`, values);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
onSave();
|
onSave();
|
||||||
|
|
|
@ -0,0 +1,262 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import Button from 'components/common/Button';
|
||||||
|
import DateFilter from 'components/common/DateFilter';
|
||||||
|
import DropDown from 'components/common/DropDown';
|
||||||
|
import FormLayout, {
|
||||||
|
FormButtons,
|
||||||
|
FormError,
|
||||||
|
FormMessage,
|
||||||
|
FormRow,
|
||||||
|
} from 'components/layout/FormLayout';
|
||||||
|
import DataTable from 'components/metrics/DataTable';
|
||||||
|
import FilterTags from 'components/metrics/FilterTags';
|
||||||
|
import { Field, Form, Formik } from 'formik';
|
||||||
|
import useApi from 'hooks/useApi';
|
||||||
|
import useDateRange from 'hooks/useDateRange';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import styles from './EventDataForm.module.css';
|
||||||
|
import useTimezone from 'hooks/useTimezone';
|
||||||
|
|
||||||
|
export const filterOptions = [
|
||||||
|
{ label: 'Count', value: 'count' },
|
||||||
|
{ label: 'Average', value: 'avg' },
|
||||||
|
{ label: 'Minimum', value: 'min' },
|
||||||
|
{ label: 'Maxmimum', value: 'max' },
|
||||||
|
{ label: 'Sum', value: 'sum' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const dateOptions = [
|
||||||
|
{ label: <FormattedMessage id="label.today" defaultMessage="Today" />, value: '1day' },
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-hours" defaultMessage="Last {x} hours" values={{ x: 24 }} />
|
||||||
|
),
|
||||||
|
value: '24hour',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.yesterday" defaultMessage="Yesterday" />,
|
||||||
|
value: '-1day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.this-week" defaultMessage="This week" />,
|
||||||
|
value: '1week',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 7 }} />
|
||||||
|
),
|
||||||
|
value: '7day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.this-month" defaultMessage="This month" />,
|
||||||
|
value: '1month',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 30 }} />
|
||||||
|
),
|
||||||
|
value: '30day',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<FormattedMessage id="label.last-days" defaultMessage="Last {x} days" values={{ x: 90 }} />
|
||||||
|
),
|
||||||
|
value: '90day',
|
||||||
|
},
|
||||||
|
{ label: <FormattedMessage id="label.this-year" defaultMessage="This year" />, value: '1year' },
|
||||||
|
{
|
||||||
|
label: <FormattedMessage id="label.custom-range" defaultMessage="Custom range" />,
|
||||||
|
value: 'custom',
|
||||||
|
divider: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function EventDataForm({ websiteId, onClose, className }) {
|
||||||
|
const { post } = useApi();
|
||||||
|
const [message, setMessage] = useState();
|
||||||
|
const [columns, setColumns] = useState({});
|
||||||
|
const [filters, setFilters] = useState({});
|
||||||
|
const [data, setData] = useState([]);
|
||||||
|
const [dateRange, setDateRange] = useDateRange('report');
|
||||||
|
const { startDate, endDate, value } = dateRange;
|
||||||
|
const [timezone] = useTimezone();
|
||||||
|
const [isValid, setIsValid] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (Object.keys(columns).length > 0) {
|
||||||
|
setIsValid(true);
|
||||||
|
} else {
|
||||||
|
setIsValid(false);
|
||||||
|
}
|
||||||
|
}, [columns]);
|
||||||
|
|
||||||
|
const handleAddTag = (value, list, setState, resetForm) => {
|
||||||
|
setState({ ...list, [`${value.field}`]: value.value });
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveTag = (value, list, setState) => {
|
||||||
|
const newList = { ...list };
|
||||||
|
|
||||||
|
delete newList[`${value}`];
|
||||||
|
|
||||||
|
setState(newList);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
const params = {
|
||||||
|
website_id: websiteId,
|
||||||
|
start_at: +startDate,
|
||||||
|
end_at: +endDate,
|
||||||
|
timezone,
|
||||||
|
columns,
|
||||||
|
filters,
|
||||||
|
};
|
||||||
|
|
||||||
|
const { ok, data } = await post(`/websites/${websiteId}/eventdata`, params);
|
||||||
|
|
||||||
|
if (!ok) {
|
||||||
|
setMessage(<FormattedMessage id="message.failure" defaultMessage="Something went wrong." />);
|
||||||
|
setData([]);
|
||||||
|
} else {
|
||||||
|
setData(data);
|
||||||
|
setMessage(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormMessage>{message}</FormMessage>
|
||||||
|
<div className={classNames(styles.container, className)}>
|
||||||
|
<div className={styles.form}>
|
||||||
|
<FormLayout>
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="date-range">
|
||||||
|
<FormattedMessage id="label.date-range" defaultMessage="Date Range" />
|
||||||
|
</label>
|
||||||
|
<DateFilter
|
||||||
|
value={value}
|
||||||
|
startDate={startDate}
|
||||||
|
endDate={endDate}
|
||||||
|
onChange={setDateRange}
|
||||||
|
options={dateOptions}
|
||||||
|
/>
|
||||||
|
</FormRow>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ field: '', value: '' }}
|
||||||
|
onSubmit={(value, { resetForm }) =>
|
||||||
|
handleAddTag(value, columns, setColumns, resetForm)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ values, setFieldValue }) => (
|
||||||
|
<Form>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="field">
|
||||||
|
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<Field name="field" type="text" />
|
||||||
|
<FormError name="field" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="value">
|
||||||
|
<FormattedMessage id="label.type" defaultMessage="Type" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<DropDown
|
||||||
|
value={values.value}
|
||||||
|
onChange={value => setFieldValue('value', value)}
|
||||||
|
className={styles.dropdown}
|
||||||
|
name="value"
|
||||||
|
options={filterOptions}
|
||||||
|
/>
|
||||||
|
<FormError name="value" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormButtons className={styles.formButtons}>
|
||||||
|
<Button
|
||||||
|
variant="action"
|
||||||
|
type="submit"
|
||||||
|
disabled={!values.field || !values.value}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="label.add-column" defaultMessage="Add Column" />
|
||||||
|
</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
<FilterTags
|
||||||
|
className={styles.filterTag}
|
||||||
|
params={columns}
|
||||||
|
onClick={value => handleRemoveTag(value, columns, setColumns)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.filters}>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ field: '', value: '' }}
|
||||||
|
onSubmit={(value, { resetForm }) =>
|
||||||
|
handleAddTag(value, filters, setFilters, resetForm)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{({ values }) => (
|
||||||
|
<Form>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="field">
|
||||||
|
<FormattedMessage id="label.field-name" defaultMessage="Field Name" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<Field name="field" type="text" />
|
||||||
|
<FormError name="field" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormRow>
|
||||||
|
<label htmlFor="value">
|
||||||
|
<FormattedMessage id="label.value" defaultMessage="Value" />
|
||||||
|
</label>
|
||||||
|
<div>
|
||||||
|
<Field name="value" type="text" />
|
||||||
|
<FormError name="value" />
|
||||||
|
</div>
|
||||||
|
</FormRow>
|
||||||
|
<FormButtons className={styles.formButtons}>
|
||||||
|
<Button
|
||||||
|
variant="action"
|
||||||
|
type="submit"
|
||||||
|
disabled={!values.field || !values.value}
|
||||||
|
>
|
||||||
|
<FormattedMessage id="label.add-filter" defaultMessage="Add Filter" />
|
||||||
|
</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
<FilterTags
|
||||||
|
className={styles.filterTag}
|
||||||
|
params={filters}
|
||||||
|
onClick={value => handleRemoveTag(value, filters, setFilters)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</FormLayout>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<DataTable className={styles.table} data={data} title="Results" showPercentage={false} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FormButtons>
|
||||||
|
<Button variant="action" onClick={handleSubmit} disabled={!isValid}>
|
||||||
|
<FormattedMessage id="label.search" defaultMessage="Search" />
|
||||||
|
</Button>
|
||||||
|
<Button onClick={onClose}>
|
||||||
|
<FormattedMessage id="label.cancel" defaultMessage="Cancel" />
|
||||||
|
</Button>
|
||||||
|
</FormButtons>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
border-right: 1px solid var(--gray300);
|
||||||
|
width: 420px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
padding: 10px 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filters + .filters {
|
||||||
|
border-top: 1px solid var(--gray300);
|
||||||
|
min-height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
padding: 10px;
|
||||||
|
min-height: 430px;
|
||||||
|
min-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formButtons {
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
min-height: 39px;
|
||||||
|
min-width: 240px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filterTag {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin: 10px 5px 5px 5px;
|
||||||
|
}
|
|
@ -8,7 +8,7 @@ import CopyButton from 'components/common/CopyButton';
|
||||||
export default function TrackingCodeForm({ values, onClose }) {
|
export default function TrackingCodeForm({ values, onClose }) {
|
||||||
const ref = useRef();
|
const ref = useRef();
|
||||||
const { basePath } = useRouter();
|
const { basePath } = useRouter();
|
||||||
const { name, share_id } = values;
|
const { name, shareId } = values;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormLayout>
|
<FormLayout>
|
||||||
|
@ -27,7 +27,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
defaultValue={`${
|
defaultValue={`${
|
||||||
document.location.origin
|
document.location.origin
|
||||||
}${basePath}/share/${share_id}/${encodeURIComponent(name)}`}
|
}${basePath}/share/${shareId}/${encodeURIComponent(name)}`}
|
||||||
readOnly
|
readOnly
|
||||||
/>
|
/>
|
||||||
</FormRow>
|
</FormRow>
|
||||||
|
|
|
@ -26,7 +26,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
||||||
rows={3}
|
rows={3}
|
||||||
cols={60}
|
cols={60}
|
||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
defaultValue={`<script async defer data-website-id="${values.website_uuid}" src="${
|
defaultValue={`<script async defer data-website-id="${values.websiteUuid}" src="${
|
||||||
document.location.origin
|
document.location.origin
|
||||||
}${basePath}/${trackerScriptName ? `${trackerScriptName}.js` : 'umami.js'}"></script>`}
|
}${basePath}/${trackerScriptName ? `${trackerScriptName}.js` : 'umami.js'}"></script>`}
|
||||||
readOnly
|
readOnly
|
||||||
|
|
|
@ -38,18 +38,17 @@ const validate = ({ name, domain }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const OwnerDropDown = ({ user, accounts }) => {
|
const OwnerDropDown = ({ user, accounts }) => {
|
||||||
console.info(styles);
|
|
||||||
const { setFieldValue, values } = useFormikContext();
|
const { setFieldValue, values } = useFormikContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (values.user_id != null && values.owner === '') {
|
if (values.userId != null && values.owner === '') {
|
||||||
setFieldValue('owner', values.user_id.toString());
|
setFieldValue('owner', values.userId.toString());
|
||||||
} else if (user?.user_id && values.owner === '') {
|
} else if (user?.id && values.owner === '') {
|
||||||
setFieldValue('owner', user.user_id.toString());
|
setFieldValue('owner', user.id.toString());
|
||||||
}
|
}
|
||||||
}, [accounts, setFieldValue, user, values]);
|
}, [accounts, setFieldValue, user, values]);
|
||||||
|
|
||||||
if (user?.is_admin) {
|
if (user?.isAdmin) {
|
||||||
return (
|
return (
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<label htmlFor="owner">
|
<label htmlFor="owner">
|
||||||
|
@ -58,7 +57,7 @@ const OwnerDropDown = ({ user, accounts }) => {
|
||||||
<div>
|
<div>
|
||||||
<Field as="select" name="owner" className={styles.dropdown}>
|
<Field as="select" name="owner" className={styles.dropdown}>
|
||||||
{accounts?.map(acc => (
|
{accounts?.map(acc => (
|
||||||
<option key={acc.user_id} value={acc.user_id}>
|
<option key={acc.id} value={acc.id}>
|
||||||
{acc.username}
|
{acc.username}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
|
@ -79,7 +78,9 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
|
|
||||||
const handleSubmit = async values => {
|
const handleSubmit = async values => {
|
||||||
const { ok, data } = await post('/website', values);
|
const { id: websiteId } = values;
|
||||||
|
|
||||||
|
const { ok, data } = await post(websiteId ? `/websites/${websiteId}` : '/websites', values);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
onSave();
|
onSave();
|
||||||
|
@ -93,7 +94,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||||
return (
|
return (
|
||||||
<FormLayout>
|
<FormLayout>
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{ ...initialValues, ...values, enable_share_url: !!values?.share_id }}
|
initialValues={{ ...initialValues, ...values, enableShareUrl: !!values?.shareId }}
|
||||||
validate={validate}
|
validate={validate}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
|
@ -117,9 +118,9 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||||
name="domain"
|
name="domain"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="example.com"
|
placeholder="example.com"
|
||||||
spellcheck="false"
|
spellCheck="false"
|
||||||
autocapitalize="off"
|
autoCapitalize="off"
|
||||||
autocorrect="off"
|
autoCorrect="off"
|
||||||
/>
|
/>
|
||||||
<FormError name="domain" />
|
<FormError name="domain" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -127,7 +128,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||||
<OwnerDropDown accounts={accounts} user={user} />
|
<OwnerDropDown accounts={accounts} user={user} />
|
||||||
<FormRow>
|
<FormRow>
|
||||||
<label />
|
<label />
|
||||||
<Field name="enable_share_url">
|
<Field name="enableShareUrl">
|
||||||
{({ field }) => (
|
{({ field }) => (
|
||||||
<Checkbox
|
<Checkbox
|
||||||
{...field}
|
{...field}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default function Header() {
|
||||||
const { pathname } = useRouter();
|
const { pathname } = useRouter();
|
||||||
const { updatesDisabled } = useConfig();
|
const { updatesDisabled } = useConfig();
|
||||||
const isSharePage = pathname.includes('/share/');
|
const isSharePage = pathname.includes('/share/');
|
||||||
const allowUpdate = user?.is_admin && !updatesDisabled && !isSharePage;
|
const allowUpdate = user?.isAdmin && !updatesDisabled && !isSharePage;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -38,9 +38,11 @@ export default function Header() {
|
||||||
<Link href="/realtime">
|
<Link href="/realtime">
|
||||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||||
</Link>
|
</Link>
|
||||||
|
{!process.env.isCloudMode && (
|
||||||
<Link href="/settings">
|
<Link href="/settings">
|
||||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||||
</Link>
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className={styles.buttons}>
|
<div className={styles.buttons}>
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Dot from 'components/common/Dot';
|
||||||
import styles from './ActiveUsers.module.css';
|
import styles from './ActiveUsers.module.css';
|
||||||
|
|
||||||
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
|
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
|
||||||
const url = websiteId ? `/website/${websiteId}/active` : null;
|
const url = websiteId ? `/websites/${websiteId}/active` : null;
|
||||||
const { data } = useFetch(url, {
|
const { data } = useFetch(url, {
|
||||||
interval,
|
interval,
|
||||||
});
|
});
|
||||||
|
|
|
@ -16,6 +16,7 @@ export default function DataTable({
|
||||||
height,
|
height,
|
||||||
animate = true,
|
animate = true,
|
||||||
virtualize = false,
|
virtualize = false,
|
||||||
|
showPercentage = true,
|
||||||
}) {
|
}) {
|
||||||
const [format, setFormat] = useState(true);
|
const [format, setFormat] = useState(true);
|
||||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||||
|
@ -38,6 +39,7 @@ export default function DataTable({
|
||||||
animate={animate && !virtualize}
|
animate={animate && !virtualize}
|
||||||
format={formatFunc}
|
format={formatFunc}
|
||||||
onClick={handleSetFormat}
|
onClick={handleSetFormat}
|
||||||
|
showPercentage={showPercentage}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -68,7 +70,15 @@ export default function DataTable({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) => {
|
const AnimatedRow = ({
|
||||||
|
label,
|
||||||
|
value = 0,
|
||||||
|
percent,
|
||||||
|
animate,
|
||||||
|
format,
|
||||||
|
onClick,
|
||||||
|
showPercentage = true,
|
||||||
|
}) => {
|
||||||
const props = useSpring({
|
const props = useSpring({
|
||||||
width: percent,
|
width: percent,
|
||||||
y: value,
|
y: value,
|
||||||
|
@ -82,6 +92,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) =>
|
||||||
<div className={styles.value} onClick={onClick}>
|
<div className={styles.value} onClick={onClick}>
|
||||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||||
</div>
|
</div>
|
||||||
|
{showPercentage && (
|
||||||
<div className={styles.percent}>
|
<div className={styles.percent}>
|
||||||
<animated.div
|
<animated.div
|
||||||
className={styles.bar}
|
className={styles.bar}
|
||||||
|
@ -91,6 +102,7 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) =>
|
||||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||||
</animated.span>
|
</animated.span>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data, loading } = useFetch(
|
const { data, loading } = useFetch(
|
||||||
`/website/${websiteId}/events`,
|
`/websites/${websiteId}/events`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
|
|
|
@ -5,12 +5,12 @@ import Button from 'components/common/Button';
|
||||||
import Times from 'assets/times.svg';
|
import Times from 'assets/times.svg';
|
||||||
import styles from './FilterTags.module.css';
|
import styles from './FilterTags.module.css';
|
||||||
|
|
||||||
export default function FilterTags({ params, onClick }) {
|
export default function FilterTags({ className, params, onClick }) {
|
||||||
if (Object.keys(params).filter(key => params[key]).length === 0) {
|
if (Object.keys(params).filter(key => params[key]).length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<div className={classNames(styles.filters, 'col-12')}>
|
<div className={classNames(styles.filters, 'col-12', className)}>
|
||||||
{Object.keys(params).map(key => {
|
{Object.keys(params).map(key => {
|
||||||
if (!params[key]) {
|
if (!params[key]) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -7,8 +7,5 @@
|
||||||
.tag {
|
.tag {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
margin-right: 20px;
|
||||||
|
|
||||||
.tag + .tag {
|
|
||||||
margin-left: 20px;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default function MetricsBar({ websiteId, className }) {
|
||||||
} = usePageQuery();
|
} = usePageQuery();
|
||||||
|
|
||||||
const { data, error, loading } = useFetch(
|
const { data, error, loading } = useFetch(
|
||||||
`/website/${websiteId}/stats`,
|
`/websites/${websiteId}/stats`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
|
|
|
@ -38,7 +38,7 @@ export default function MetricsTable({
|
||||||
const { formatMessage } = useIntl();
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
const { data, loading, error } = useFetch(
|
const { data, loading, error } = useFetch(
|
||||||
`/website/${websiteId}/metrics`,
|
`/websites/${websiteId}/metrics`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
type,
|
type,
|
||||||
|
|
|
@ -9,8 +9,8 @@ function mapData(data) {
|
||||||
const arr = [];
|
const arr = [];
|
||||||
|
|
||||||
data.reduce((obj, val) => {
|
data.reduce((obj, val) => {
|
||||||
const { created_at } = val;
|
const { createdAt } = val;
|
||||||
const t = startOfMinute(parseISO(created_at));
|
const t = startOfMinute(parseISO(createdAt));
|
||||||
if (t.getTime() > last) {
|
if (t.getTime() > last) {
|
||||||
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
||||||
arr.push(obj);
|
arr.push(obj);
|
||||||
|
|
|
@ -11,9 +11,9 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
|
||||||
const options = [
|
const options = [
|
||||||
{ label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />, value: 0 },
|
{ label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />, value: 0 },
|
||||||
].concat(
|
].concat(
|
||||||
websites.map(({ name, website_id }, index) => ({
|
websites.map(({ name, id }, index) => ({
|
||||||
label: name,
|
label: name,
|
||||||
value: website_id,
|
value: id,
|
||||||
divider: index === 0,
|
divider: index === 0,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
|
@ -22,7 +22,7 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
|
||||||
|
|
||||||
const count = useMemo(() => {
|
const count = useMemo(() => {
|
||||||
return sessions.filter(
|
return sessions.filter(
|
||||||
({ created_at }) => differenceInMinutes(new Date(), new Date(created_at)) <= 5,
|
({ createdAt }) => differenceInMinutes(new Date(), new Date(createdAt)) <= 5,
|
||||||
).length;
|
).length;
|
||||||
}, [sessions, websiteId]);
|
}, [sessions, websiteId]);
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ import Visitor from 'assets/visitor.svg';
|
||||||
import Eye from 'assets/eye.svg';
|
import Eye from 'assets/eye.svg';
|
||||||
import { stringToColor } from 'lib/format';
|
import { stringToColor } from 'lib/format';
|
||||||
import { dateFormat } from 'lib/date';
|
import { dateFormat } from 'lib/date';
|
||||||
|
import { safeDecodeURI } from 'next-basics';
|
||||||
import styles from './RealtimeLog.module.css';
|
import styles from './RealtimeLog.module.css';
|
||||||
|
|
||||||
const TYPE_ALL = 0;
|
const TYPE_ALL = 0;
|
||||||
|
@ -36,7 +37,7 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||||
|
|
||||||
const logs = useMemo(() => {
|
const logs = useMemo(() => {
|
||||||
const { pageviews, sessions, events } = data;
|
const { pageviews, sessions, events } = data;
|
||||||
const logs = [...pageviews, ...sessions, ...events].sort(firstBy('created_at', -1));
|
const logs = [...pageviews, ...sessions, ...events].sort(firstBy('createdAt', -1));
|
||||||
if (filter) {
|
if (filter) {
|
||||||
return logs.filter(row => getType(row) === filter);
|
return logs.filter(row => getType(row) === filter);
|
||||||
}
|
}
|
||||||
|
@ -44,8 +45,8 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||||
}, [data, filter]);
|
}, [data, filter]);
|
||||||
|
|
||||||
const uuids = useMemo(() => {
|
const uuids = useMemo(() => {
|
||||||
return data.sessions.reduce((obj, { session_id, session_uuid }) => {
|
return data.sessions.reduce((obj, { sessionId, sessionUuid }) => {
|
||||||
obj[session_id] = session_uuid;
|
obj[sessionId] = sessionUuid;
|
||||||
return obj;
|
return obj;
|
||||||
}, {});
|
}, {});
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
@ -69,14 +70,14 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function getType({ view_id, session_id, event_id }) {
|
function getType({ pageviewId, sessionId, eventId }) {
|
||||||
if (event_id) {
|
if (eventId) {
|
||||||
return TYPE_EVENT;
|
return TYPE_EVENT;
|
||||||
}
|
}
|
||||||
if (view_id) {
|
if (pageviewId) {
|
||||||
return TYPE_PAGEVIEW;
|
return TYPE_PAGEVIEW;
|
||||||
}
|
}
|
||||||
if (session_id) {
|
if (sessionId) {
|
||||||
return TYPE_SESSION;
|
return TYPE_SESSION;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -86,26 +87,26 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||||
return TYPE_ICONS[getType(row)];
|
return TYPE_ICONS[getType(row)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWebsite({ website_id }) {
|
function getWebsite({ websiteId }) {
|
||||||
return websites.find(n => n.website_id === website_id);
|
return websites.find(n => n.id === websiteId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDetail({
|
function getDetail({
|
||||||
event_name,
|
eventName,
|
||||||
view_id,
|
pageviewId,
|
||||||
session_id,
|
sessionId,
|
||||||
url,
|
url,
|
||||||
browser,
|
browser,
|
||||||
os,
|
os,
|
||||||
country,
|
country,
|
||||||
device,
|
device,
|
||||||
website_id,
|
websiteId,
|
||||||
}) {
|
}) {
|
||||||
if (event_name) {
|
if (eventName) {
|
||||||
return <div>{event_name}</div>;
|
return <div>{eventName}</div>;
|
||||||
}
|
}
|
||||||
if (view_id) {
|
if (pageviewId) {
|
||||||
const domain = getWebsite({ website_id })?.domain;
|
const domain = getWebsite({ websiteId })?.domain;
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
|
@ -113,11 +114,11 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer noopener"
|
rel="noreferrer noopener"
|
||||||
>
|
>
|
||||||
{url}
|
{safeDecodeURI(url)}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (session_id) {
|
if (sessionId) {
|
||||||
return (
|
return (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id="message.log.visitor"
|
id="message.log.visitor"
|
||||||
|
@ -133,14 +134,14 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTime({ created_at }) {
|
function getTime({ createdAt }) {
|
||||||
return dateFormat(new Date(created_at), 'pp', locale);
|
return dateFormat(new Date(createdAt), 'pp', locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColor(row) {
|
function getColor(row) {
|
||||||
const { session_id } = row;
|
const { sessionId } = row;
|
||||||
|
|
||||||
return stringToColor(uuids[session_id] || `${session_id}${getWebsite(row)}`);
|
return stringToColor(uuids[sessionId] || `${sessionId}${getWebsite(row)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const Row = ({ index, style }) => {
|
const Row = ({ index, style }) => {
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
||||||
id =>
|
id =>
|
||||||
websites.length === 1
|
websites.length === 1
|
||||||
? websites[0]?.domain
|
? websites[0]?.domain
|
||||||
: websites.find(({ website_id }) => website_id === id)?.domain,
|
: websites.find(({ websiteId }) => websiteId === id)?.domain,
|
||||||
[websites],
|
[websites],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -65,10 +65,10 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
||||||
|
|
||||||
const pages = percentFilter(
|
const pages = percentFilter(
|
||||||
pageviews
|
pageviews
|
||||||
.reduce((arr, { url, website_id }) => {
|
.reduce((arr, { url, websiteId }) => {
|
||||||
if (url?.startsWith('/')) {
|
if (url?.startsWith('/')) {
|
||||||
if (!websiteId && websites.length > 1) {
|
if (!websiteId && websites.length > 1) {
|
||||||
url = `${getDomain(website_id)}${url}`;
|
url = `${getDomain(websiteId)}${url}`;
|
||||||
}
|
}
|
||||||
const row = arr.find(({ x }) => x === url);
|
const row = arr.find(({ x }) => x === url);
|
||||||
|
|
||||||
|
|
|
@ -35,7 +35,7 @@ export default function WebsiteChart({
|
||||||
const { get } = useApi();
|
const { get } = useApi();
|
||||||
|
|
||||||
const { data, loading, error } = useFetch(
|
const { data, loading, error } = useFetch(
|
||||||
`/website/${websiteId}/pageviews`,
|
`/websites/${websiteId}/pageviews`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
start_at: +startDate,
|
start_at: +startDate,
|
||||||
|
@ -70,9 +70,9 @@ export default function WebsiteChart({
|
||||||
|
|
||||||
async function handleDateChange(value) {
|
async function handleDateChange(value) {
|
||||||
if (value === 'all') {
|
if (value === 'all') {
|
||||||
const { data, ok } = await get(`/website/${websiteId}`);
|
const { data, ok } = await get(`/websites/${websiteId}`);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) });
|
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setDateRange(value);
|
setDateRange(value);
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import React from 'react';
|
import Arrow from 'assets/arrow-right.svg';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { FormattedMessage } from 'react-intl';
|
import Favicon from 'components/common/Favicon';
|
||||||
import Link from 'components/common/Link';
|
import Link from 'components/common/Link';
|
||||||
import OverflowText from 'components/common/OverflowText';
|
import OverflowText from 'components/common/OverflowText';
|
||||||
import PageHeader from 'components/layout/PageHeader';
|
|
||||||
import RefreshButton from 'components/common/RefreshButton';
|
import RefreshButton from 'components/common/RefreshButton';
|
||||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||||
import Favicon from 'components/common/Favicon';
|
import PageHeader from 'components/layout/PageHeader';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
import ActiveUsers from './ActiveUsers';
|
import ActiveUsers from './ActiveUsers';
|
||||||
import Arrow from 'assets/arrow-right.svg';
|
|
||||||
import styles from './WebsiteHeader.module.css';
|
import styles from './WebsiteHeader.module.css';
|
||||||
|
|
||||||
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
export default function WebsiteHeader({ websiteId, title, domain, showLink = false }) {
|
||||||
|
@ -17,8 +16,8 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
|
||||||
<Favicon domain={domain} />
|
<Favicon domain={domain} />
|
||||||
<Link
|
<Link
|
||||||
className={styles.titleLink}
|
className={styles.titleLink}
|
||||||
href="/website/[...id]"
|
href="/websites/[...id]"
|
||||||
as={`/website/${websiteId}/${title}`}
|
as={`/websites/${websiteId}/${title}`}
|
||||||
>
|
>
|
||||||
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -41,8 +40,8 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
|
||||||
<RefreshButton websiteId={websiteId} />
|
<RefreshButton websiteId={websiteId} />
|
||||||
{showLink && (
|
{showLink && (
|
||||||
<Link
|
<Link
|
||||||
href="/website/[...id]"
|
href="/websites/[...id]"
|
||||||
as={`/website/${websiteId}/${title}`}
|
as={`/websites/${websiteId}/${title}`}
|
||||||
className={styles.link}
|
className={styles.link}
|
||||||
icon={<Arrow />}
|
icon={<Arrow />}
|
||||||
size="small"
|
size="small"
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default function DashboardEdit({ websites }) {
|
||||||
const ordered = useMemo(
|
const ordered = useMemo(
|
||||||
() =>
|
() =>
|
||||||
websites
|
websites
|
||||||
.map(website => ({ ...website, order: order.indexOf(website.website_id) }))
|
.map(website => ({ ...website, order: order.indexOf(website.websiteId) }))
|
||||||
.sort(firstBy('order')),
|
.sort(firstBy('order')),
|
||||||
[websites, order],
|
[websites, order],
|
||||||
);
|
);
|
||||||
|
@ -36,7 +36,7 @@ export default function DashboardEdit({ websites }) {
|
||||||
const [removed] = orderedWebsites.splice(source.index, 1);
|
const [removed] = orderedWebsites.splice(source.index, 1);
|
||||||
orderedWebsites.splice(destination.index, 0, removed);
|
orderedWebsites.splice(destination.index, 0, removed);
|
||||||
|
|
||||||
setOrder(orderedWebsites.map((website) => website?.website_id || 0));
|
setOrder(orderedWebsites.map(website => website?.websiteId || 0));
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleSave() {
|
function handleSave() {
|
||||||
|
@ -76,8 +76,8 @@ export default function DashboardEdit({ websites }) {
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
|
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
|
||||||
>
|
>
|
||||||
{ordered.map(({ website_id, name, domain }, index) => (
|
{ordered.map(({ websiteId, name, domain }, index) => (
|
||||||
<Draggable key={website_id} draggableId={`${dragId}-${website_id}`} index={index}>
|
<Draggable key={websiteId} draggableId={`${dragId}-${websiteId}`} index={index}>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
|
|
|
@ -21,11 +21,11 @@ function mergeData(state, data, time) {
|
||||||
const ids = state.map(({ __id }) => __id);
|
const ids = state.map(({ __id }) => __id);
|
||||||
return state
|
return state
|
||||||
.concat(data.filter(({ __id }) => !ids.includes(__id)))
|
.concat(data.filter(({ __id }) => !ids.includes(__id)))
|
||||||
.filter(({ created_at }) => new Date(created_at).getTime() >= time);
|
.filter(({ createdAt }) => new Date(createdAt).getTime() >= time);
|
||||||
}
|
}
|
||||||
|
|
||||||
function filterWebsite(data, id) {
|
function filterWebsite(data, id) {
|
||||||
return data.filter(({ website_id }) => website_id === id);
|
return data.filter(({ websiteId }) => websiteId === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RealtimeDashboard() {
|
export default function RealtimeDashboard() {
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default function Settings() {
|
||||||
{
|
{
|
||||||
label: <FormattedMessage id="label.accounts" defaultMessage="Accounts" />,
|
label: <FormattedMessage id="label.accounts" defaultMessage="Accounts" />,
|
||||||
value: ACCOUNTS,
|
value: ACCOUNTS,
|
||||||
hidden: !user?.is_admin,
|
hidden: !user?.isAdmin,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||||
|
|
|
@ -24,9 +24,9 @@ export default function TestConsole() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = data.map(({ name, website_id }) => ({ label: name, value: website_id }));
|
const options = data.map(({ name, websiteUuid }) => ({ label: name, value: websiteUuid }));
|
||||||
const website = data.find(({ website_id }) => website_id === +websiteId);
|
const website = data.find(({ websiteUuid }) => websiteId === websiteUuid);
|
||||||
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value;
|
const selectedValue = options.find(({ value }) => value === website?.websiteUuid)?.value;
|
||||||
|
|
||||||
function handleSelect(value) {
|
function handleSelect(value) {
|
||||||
router.push(`/console/${value}`);
|
router.push(`/console/${value}`);
|
||||||
|
@ -46,7 +46,7 @@ export default function TestConsole() {
|
||||||
<script
|
<script
|
||||||
async
|
async
|
||||||
defer
|
defer
|
||||||
data-website-id={website.website_uuid}
|
data-website-id={website.websiteUuid}
|
||||||
src={`${basePath}/umami.js`}
|
src={`${basePath}/umami.js`}
|
||||||
data-cache="true"
|
data-cache="true"
|
||||||
/>
|
/>
|
||||||
|
@ -104,13 +104,13 @@ export default function TestConsole() {
|
||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="col-12">
|
<div className="col-12">
|
||||||
<WebsiteChart
|
<WebsiteChart
|
||||||
websiteId={website.website_id}
|
websiteId={website.websiteUuid}
|
||||||
title={website.name}
|
title={website.name}
|
||||||
domain={website.domain}
|
domain={website.domain}
|
||||||
showLink
|
showLink
|
||||||
/>
|
/>
|
||||||
<PageHeader>Events</PageHeader>
|
<PageHeader>Events</PageHeader>
|
||||||
<EventsChart websiteId={website.website_id} />
|
<EventsChart websiteId={website.websiteUuid} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -24,6 +24,7 @@ import useFetch from 'hooks/useFetch';
|
||||||
import usePageQuery from 'hooks/usePageQuery';
|
import usePageQuery from 'hooks/usePageQuery';
|
||||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||||
import styles from './WebsiteDetails.module.css';
|
import styles from './WebsiteDetails.module.css';
|
||||||
|
import EventDataButton from 'components/common/EventDataButton';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
|
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
|
||||||
|
@ -52,7 +53,7 @@ const views = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebsiteDetails({ websiteId }) {
|
export default function WebsiteDetails({ websiteId }) {
|
||||||
const { data } = useFetch(`/website/${websiteId}`);
|
const { data } = useFetch(`/websites/${websiteId}`);
|
||||||
const [chartLoaded, setChartLoaded] = useState(false);
|
const [chartLoaded, setChartLoaded] = useState(false);
|
||||||
const [countryData, setCountryData] = useState();
|
const [countryData, setCountryData] = useState();
|
||||||
const [eventsData, setEventsData] = useState();
|
const [eventsData, setEventsData] = useState();
|
||||||
|
@ -183,6 +184,7 @@ export default function WebsiteDetails({ websiteId }) {
|
||||||
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
<GridColumn xs={12} md={12} lg={8}>
|
<GridColumn xs={12} md={12} lg={8}>
|
||||||
|
<EventDataButton websiteId={websiteId} />
|
||||||
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
||||||
</GridColumn>
|
</GridColumn>
|
||||||
</GridRow>
|
</GridRow>
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||||
const ordered = useMemo(
|
const ordered = useMemo(
|
||||||
() =>
|
() =>
|
||||||
websites
|
websites
|
||||||
.map(website => ({ ...website, order: websiteOrder.indexOf(website.website_id) || 0 }))
|
.map(website => ({ ...website, order: websiteOrder.indexOf(website.websiteUuid) || 0 }))
|
||||||
.sort(firstBy('order')),
|
.sort(firstBy('order')),
|
||||||
[websites, websiteOrder],
|
[websites, websiteOrder],
|
||||||
);
|
);
|
||||||
|
@ -46,11 +46,11 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{ordered.map(({ website_id, name, domain }, index) =>
|
{ordered.map(({ websiteUuid, name, domain }, index) =>
|
||||||
index < limit ? (
|
index < limit ? (
|
||||||
<div key={website_id} className={styles.website}>
|
<div key={websiteUuid} className={styles.website}>
|
||||||
<WebsiteChart
|
<WebsiteChart
|
||||||
websiteId={website_id}
|
websiteId={websiteUuid}
|
||||||
title={name}
|
title={name}
|
||||||
domain={domain}
|
domain={domain}
|
||||||
showChart={showCharts}
|
showChart={showCharts}
|
||||||
|
|
|
@ -27,10 +27,10 @@ export default function AccountSettings() {
|
||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
const { data } = useFetch(`/accounts`, {}, [saved]);
|
const { data } = useFetch(`/accounts`, {}, [saved]);
|
||||||
|
|
||||||
const Checkmark = ({ is_admin }) => (is_admin ? <Icon icon={<Check />} size="medium" /> : null);
|
const Checkmark = ({ isAdmin }) => (isAdmin ? <Icon icon={<Check />} size="medium" /> : null);
|
||||||
|
|
||||||
const DashboardLink = row => (
|
const DashboardLink = row => (
|
||||||
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
|
<Link href={`/dashboard/${row.userId}/${row.username}`}>
|
||||||
<a>
|
<a>
|
||||||
<Icon icon={<LinkIcon />} />
|
<Icon icon={<LinkIcon />} />
|
||||||
</a>
|
</a>
|
||||||
|
@ -42,7 +42,7 @@ export default function AccountSettings() {
|
||||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||||
</Button>
|
</Button>
|
||||||
{!row.is_admin && (
|
{!row.isAdmin && (
|
||||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
||||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -57,7 +57,7 @@ export default function AccountSettings() {
|
||||||
className: 'col-12 col-lg-4',
|
className: 'col-12 col-lg-4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'is_admin',
|
key: 'isAdmin',
|
||||||
label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />,
|
label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />,
|
||||||
className: 'col-12 col-lg-3',
|
className: 'col-12 col-lg-3',
|
||||||
render: Checkmark,
|
render: Checkmark,
|
||||||
|
@ -121,7 +121,7 @@ export default function AccountSettings() {
|
||||||
title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
|
title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
|
||||||
>
|
>
|
||||||
<DeleteForm
|
<DeleteForm
|
||||||
values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }}
|
values={{ type: 'accounts', id: deleteAccount.id, name: deleteAccount.username }}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default function ProfileSettings() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user_id, username } = user;
|
const { userId, username } = user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -79,7 +79,7 @@ export default function ProfileSettings() {
|
||||||
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
|
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
|
||||||
>
|
>
|
||||||
<ChangePasswordForm
|
<ChangePasswordForm
|
||||||
values={{ user_id }}
|
values={{ userId }}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onClose={() => setChangePassword(false)}
|
onClose={() => setChangePassword(false)}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -25,7 +25,11 @@ export default function UserButton() {
|
||||||
value: 'username',
|
value: 'username',
|
||||||
className: styles.username,
|
className: styles.username,
|
||||||
},
|
},
|
||||||
{ label: <FormattedMessage id="label.profile" defaultMessage="Profile" />, value: 'profile' },
|
{
|
||||||
|
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||||
|
value: 'profile',
|
||||||
|
hidden: process.env.isCloudMode,
|
||||||
|
},
|
||||||
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
|
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
|
@ -37,16 +37,16 @@ export default function WebsiteSettings() {
|
||||||
const [saved, setSaved] = useState(0);
|
const [saved, setSaved] = useState(0);
|
||||||
const [message, setMessage] = useState();
|
const [message, setMessage] = useState();
|
||||||
|
|
||||||
const { data } = useFetch('/websites', { params: { include_all: !!user?.is_admin } }, [saved]);
|
const { data } = useFetch('/websites', { params: { include_all: !!user?.isAdmin } }, [saved]);
|
||||||
|
|
||||||
const Buttons = row => (
|
const Buttons = row => (
|
||||||
<ButtonLayout align="right">
|
<ButtonLayout align="right">
|
||||||
{row.share_id && (
|
{row.shareId && (
|
||||||
<Button
|
<Button
|
||||||
icon={<LinkIcon />}
|
icon={<LinkIcon />}
|
||||||
size="small"
|
size="small"
|
||||||
tooltip={<FormattedMessage id="message.get-share-url" defaultMessage="Get share URL" />}
|
tooltip={<FormattedMessage id="message.get-share-url" defaultMessage="Get share URL" />}
|
||||||
tooltipId={`button-share-${row.website_id}`}
|
tooltipId={`button-share-${row.websiteUuid}`}
|
||||||
onClick={() => setShowUrl(row)}
|
onClick={() => setShowUrl(row)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
@ -56,46 +56,46 @@ export default function WebsiteSettings() {
|
||||||
tooltip={
|
tooltip={
|
||||||
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
|
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
|
||||||
}
|
}
|
||||||
tooltipId={`button-code-${row.website_id}`}
|
tooltipId={`button-code-${row.websiteUuid}`}
|
||||||
onClick={() => setShowCode(row)}
|
onClick={() => setShowCode(row)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<Pen />}
|
icon={<Pen />}
|
||||||
size="small"
|
size="small"
|
||||||
tooltip={<FormattedMessage id="label.edit" defaultMessage="Edit" />}
|
tooltip={<FormattedMessage id="label.edit" defaultMessage="Edit" />}
|
||||||
tooltipId={`button-edit-${row.website_id}`}
|
tooltipId={`button-edit-${row.websiteUuid}`}
|
||||||
onClick={() => setEditWebsite(row)}
|
onClick={() => setEditWebsite(row)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<Reset />}
|
icon={<Reset />}
|
||||||
size="small"
|
size="small"
|
||||||
tooltip={<FormattedMessage id="label.reset" defaultMessage="Reset" />}
|
tooltip={<FormattedMessage id="label.reset" defaultMessage="Reset" />}
|
||||||
tooltipId={`button-reset-${row.website_id}`}
|
tooltipId={`button-reset-${row.websiteUuid}`}
|
||||||
onClick={() => setResetWebsite(row)}
|
onClick={() => setResetWebsite(row)}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
icon={<Trash />}
|
icon={<Trash />}
|
||||||
size="small"
|
size="small"
|
||||||
tooltip={<FormattedMessage id="label.delete" defaultMessage="Delete" />}
|
tooltip={<FormattedMessage id="label.delete" defaultMessage="Delete" />}
|
||||||
tooltipId={`button-delete-${row.website_id}`}
|
tooltipId={`button-delete-${row.websiteUuid}`}
|
||||||
onClick={() => setDeleteWebsite(row)}
|
onClick={() => setDeleteWebsite(row)}
|
||||||
/>
|
/>
|
||||||
</ButtonLayout>
|
</ButtonLayout>
|
||||||
);
|
);
|
||||||
|
|
||||||
const DetailsLink = ({ website_id, name, domain }) => (
|
const DetailsLink = ({ websiteUuid, name, domain }) => (
|
||||||
<Link
|
<Link
|
||||||
className={styles.detailLink}
|
className={styles.detailLink}
|
||||||
href="/website/[...id]"
|
href="/websites/[...id]"
|
||||||
as={`/website/${website_id}/${name}`}
|
as={`/websites/${websiteUuid}/${name}`}
|
||||||
>
|
>
|
||||||
<Favicon domain={domain} />
|
<Favicon domain={domain} />
|
||||||
<OverflowText tooltipId={`${website_id}-name`}>{name}</OverflowText>
|
<OverflowText tooltipId={`${websiteUuid}-name`}>{name}</OverflowText>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
const Domain = ({ domain, website_id }) => (
|
const Domain = ({ domain, websiteUuid }) => (
|
||||||
<OverflowText tooltipId={`${website_id}-domain`}>{domain}</OverflowText>
|
<OverflowText tooltipId={`${websiteUuid}-domain`}>{domain}</OverflowText>
|
||||||
);
|
);
|
||||||
|
|
||||||
const adminColumns = [
|
const adminColumns = [
|
||||||
|
@ -187,7 +187,7 @@ export default function WebsiteSettings() {
|
||||||
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
|
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
|
||||||
</Button>
|
</Button>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
<Table columns={user.is_admin ? adminColumns : columns} rows={data} empty={empty} />
|
<Table columns={user.isAdmin ? adminColumns : columns} rows={data} empty={empty} />
|
||||||
{editWebsite && (
|
{editWebsite && (
|
||||||
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
|
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
|
||||||
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
|
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
|
||||||
|
@ -203,7 +203,7 @@ export default function WebsiteSettings() {
|
||||||
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
|
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
|
||||||
>
|
>
|
||||||
<ResetForm
|
<ResetForm
|
||||||
values={{ type: 'website', id: resetWebsite.website_id, name: resetWebsite.name }}
|
values={{ type: 'websites', id: resetWebsite.websiteUuid, name: resetWebsite.name }}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>
|
/>
|
||||||
|
@ -214,7 +214,7 @@ export default function WebsiteSettings() {
|
||||||
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
|
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
|
||||||
>
|
>
|
||||||
<DeleteForm
|
<DeleteForm
|
||||||
values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }}
|
values={{ type: 'websites', id: deleteWebsite.websiteUuid, name: deleteWebsite.name }}
|
||||||
onSave={handleSave}
|
onSave={handleSave}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
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,70 @@
|
||||||
|
SET allow_experimental_object_type = 1;
|
||||||
|
|
||||||
|
-- Create Event
|
||||||
|
CREATE TABLE event
|
||||||
|
(
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
event_id Nullable(UUID),
|
||||||
|
--session
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
browser LowCardinality(String),
|
||||||
|
os LowCardinality(String),
|
||||||
|
device LowCardinality(String),
|
||||||
|
screen LowCardinality(String),
|
||||||
|
language LowCardinality(String),
|
||||||
|
country LowCardinality(String),
|
||||||
|
--pageview
|
||||||
|
url String,
|
||||||
|
referrer String,
|
||||||
|
--event
|
||||||
|
event_name String,
|
||||||
|
event_data JSON,
|
||||||
|
created_at DateTime('UTC')
|
||||||
|
)
|
||||||
|
engine = MergeTree
|
||||||
|
ORDER BY (website_id, session_id, created_at)
|
||||||
|
SETTINGS index_granularity = 8192;
|
||||||
|
|
||||||
|
CREATE TABLE event_queue (
|
||||||
|
website_id UUID,
|
||||||
|
session_id UUID,
|
||||||
|
event_id Nullable(UUID),
|
||||||
|
url String,
|
||||||
|
referrer String,
|
||||||
|
hostname LowCardinality(String),
|
||||||
|
browser LowCardinality(String),
|
||||||
|
os LowCardinality(String),
|
||||||
|
device LowCardinality(String),
|
||||||
|
screen LowCardinality(String),
|
||||||
|
language LowCardinality(String),
|
||||||
|
country LowCardinality(String),
|
||||||
|
event_name String,
|
||||||
|
event_data String,
|
||||||
|
created_at DateTime('UTC')
|
||||||
|
)
|
||||||
|
ENGINE = Kafka
|
||||||
|
SETTINGS kafka_broker_list = 'domain:9092,domain:9093,domain:9094', -- 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 website_id,
|
||||||
|
session_id,
|
||||||
|
event_id,
|
||||||
|
url,
|
||||||
|
referrer,
|
||||||
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
device,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
event_name,
|
||||||
|
event_data,
|
||||||
|
created_at
|
||||||
|
FROM event_queue;
|
|
@ -0,0 +1,35 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `account` ADD COLUMN `account_uuid` VARCHAR(36);
|
||||||
|
|
||||||
|
-- Backfill UUID
|
||||||
|
UPDATE `account` SET account_uuid=(SELECT uuid());
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `account` MODIFY `account_uuid` VARCHAR(36) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX `account_account_uuid_key` ON `account`(`account_uuid`);
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `event` ADD COLUMN `event_uuid` VARCHAR(36);
|
||||||
|
|
||||||
|
-- Backfill UUID
|
||||||
|
UPDATE `event` SET event_uuid=(SELECT uuid());
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `event` MODIFY `event_uuid` VARCHAR(36) NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX `event_event_uuid_key` ON `event`(`event_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `account_account_uuid_idx` ON `account`(`account_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `session_session_uuid_idx` ON `session`(`session_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `website_website_uuid_idx` ON `website`(`website_uuid`);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX `event_event_uuid_idx` ON `event`(`event_uuid`);
|
|
@ -8,60 +8,67 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model account {
|
model account {
|
||||||
user_id Int @id @default(autoincrement()) @db.UnsignedInt
|
id Int @id @default(autoincrement()) @map("user_id") @db.UnsignedInt
|
||||||
username String @unique() @db.VarChar(255)
|
username String @unique() @db.VarChar(255)
|
||||||
password String @db.VarChar(60)
|
password String @db.VarChar(60)
|
||||||
is_admin Boolean @default(false)
|
isAdmin Boolean @default(false) @map("is_admin")
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
updated_at DateTime? @default(now()) @db.Timestamp(0)
|
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamp(0)
|
||||||
|
accountUuid String @unique() @map("account_uuid") @db.VarChar(36)
|
||||||
website website[]
|
website website[]
|
||||||
|
|
||||||
|
@@index([accountUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model event {
|
model event {
|
||||||
event_id Int @id @default(autoincrement()) @db.UnsignedInt
|
id Int @id @default(autoincrement()) @map("event_id") @db.UnsignedInt
|
||||||
website_id Int @db.UnsignedInt
|
websiteId Int @map("website_id") @db.UnsignedInt
|
||||||
session_id Int @db.UnsignedInt
|
sessionId Int @map("session_id") @db.UnsignedInt
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
event_name String @db.VarChar(50)
|
eventName String @map("event_name") @db.VarChar(50)
|
||||||
session session @relation(fields: [session_id], references: [session_id])
|
eventUuid String @unique() @map("event_uuid") @db.VarChar(36)
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
session session @relation(fields: [sessionId], references: [id])
|
||||||
event_data event_data?
|
website website @relation(fields: [websiteId], references: [id])
|
||||||
|
eventData eventData?
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([createdAt])
|
||||||
@@index([session_id])
|
@@index([sessionId])
|
||||||
@@index([website_id])
|
@@index([websiteId])
|
||||||
|
@@index([eventUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model event_data {
|
model eventData {
|
||||||
event_data_id Int @id @default(autoincrement()) @db.UnsignedInt
|
id Int @id @default(autoincrement()) @map("event_data_id") @db.UnsignedInt
|
||||||
event_id Int @unique @db.UnsignedInt
|
eventId Int @unique @map("event_id") @db.UnsignedInt
|
||||||
event_data Json
|
eventData Json @map("event_data")
|
||||||
event event @relation(fields: [event_id], references: [event_id])
|
event event @relation(fields: [eventId], references: [id])
|
||||||
|
|
||||||
|
@@map("event_data")
|
||||||
}
|
}
|
||||||
|
|
||||||
model pageview {
|
model pageview {
|
||||||
view_id Int @id @default(autoincrement()) @db.UnsignedInt
|
id Int @id @default(autoincrement()) @map("view_id") @db.UnsignedInt
|
||||||
website_id Int @db.UnsignedInt
|
websiteId Int @map("website_id") @db.UnsignedInt
|
||||||
session_id Int @db.UnsignedInt
|
sessionId Int @map("session_id") @db.UnsignedInt
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @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])
|
session session @relation(fields: [sessionId], references: [id])
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [websiteId], references: [id])
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([createdAt])
|
||||||
@@index([session_id])
|
@@index([sessionId])
|
||||||
@@index([website_id, created_at])
|
@@index([websiteId, createdAt])
|
||||||
@@index([website_id])
|
@@index([websiteId])
|
||||||
@@index([website_id, session_id, created_at])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
model session {
|
model session {
|
||||||
session_id Int @id @default(autoincrement()) @db.UnsignedInt
|
id Int @id @default(autoincrement()) @map("session_id") @db.UnsignedInt
|
||||||
session_uuid String @unique() @db.VarChar(36)
|
sessionUuid String @unique() @map("session_uuid") @db.VarChar(36)
|
||||||
website_id Int @db.UnsignedInt
|
websiteId Int @map("website_id") @db.UnsignedInt
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
hostname String? @db.VarChar(100)
|
hostname String? @db.VarChar(100)
|
||||||
browser String? @db.VarChar(20)
|
browser String? @db.VarChar(20)
|
||||||
os String? @db.VarChar(20)
|
os String? @db.VarChar(20)
|
||||||
|
@ -69,26 +76,28 @@ 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])
|
website website @relation(fields: [websiteId], references: [id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([createdAt])
|
||||||
@@index([website_id])
|
@@index([websiteId])
|
||||||
|
@@index([sessionUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model website {
|
model website {
|
||||||
website_id Int @id @default(autoincrement()) @db.UnsignedInt
|
id Int @id @default(autoincrement()) @map("website_id") @db.UnsignedInt
|
||||||
website_uuid String @unique() @db.VarChar(36)
|
websiteUuid String @unique() @map("website_uuid") @db.VarChar(36)
|
||||||
user_id Int @db.UnsignedInt
|
userId Int @map("user_id") @db.UnsignedInt
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
domain String? @db.VarChar(500)
|
domain String? @db.VarChar(500)
|
||||||
share_id String? @unique() @db.VarChar(64)
|
shareId String? @unique() @map("share_id") @db.VarChar(64)
|
||||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||||
account account @relation(fields: [user_id], references: [user_id])
|
account account @relation(fields: [userId], references: [id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
session session[]
|
session session[]
|
||||||
|
|
||||||
@@index([user_id])
|
@@index([userId])
|
||||||
|
@@index([websiteUuid])
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "account" ADD COLUMN "account_uuid" UUID NULL;
|
||||||
|
|
||||||
|
-- Backfill UUID
|
||||||
|
UPDATE "account" SET account_uuid = gen_random_uuid();
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "account" ALTER COLUMN "account_uuid" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "account_account_uuid_key" ON "account"("account_uuid");
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "event" ADD COLUMN "event_uuid" UUID NULL;
|
||||||
|
|
||||||
|
-- Backfill UUID
|
||||||
|
UPDATE "event" SET event_uuid = gen_random_uuid();
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "event" ALTER COLUMN "event_uuid" SET NOT NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "event_event_uuid_key" ON "event"("event_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "account_account_uuid_idx" ON "account"("account_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "session_session_uuid_idx" ON "session"("session_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "website_website_uuid_idx" ON "website"("website_uuid");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "event_event_uuid_idx" ON "event"("event_uuid");
|
|
@ -8,60 +8,67 @@ datasource db {
|
||||||
}
|
}
|
||||||
|
|
||||||
model account {
|
model account {
|
||||||
user_id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement()) @map("user_id")
|
||||||
username String @unique @db.VarChar(255)
|
username String @unique @db.VarChar(255)
|
||||||
password String @db.VarChar(60)
|
password String @db.VarChar(60)
|
||||||
is_admin Boolean @default(false)
|
isAdmin Boolean @default(false) @map("is_admin")
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamptz(6)
|
||||||
|
accountUuid String @unique @map("account_uuid") @db.Uuid
|
||||||
website website[]
|
website website[]
|
||||||
|
|
||||||
|
@@index([accountUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model event {
|
model event {
|
||||||
event_id Int @id @default(autoincrement())
|
id Int @id() @default(autoincrement()) @map("event_id")
|
||||||
website_id Int
|
websiteId Int @map("website_id")
|
||||||
session_id Int
|
sessionId Int @map("session_id")
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
url String @db.VarChar(500)
|
url String @db.VarChar(500)
|
||||||
event_name String @db.VarChar(50)
|
eventName String @map("event_name") @db.VarChar(50)
|
||||||
session session @relation(fields: [session_id], references: [session_id])
|
eventUuid String @unique @map("event_uuid") @db.Uuid
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
session session @relation(fields: [sessionId], references: [id])
|
||||||
event_data event_data?
|
website website @relation(fields: [websiteId], references: [id])
|
||||||
|
eventData eventData?
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([createdAt])
|
||||||
@@index([session_id])
|
@@index([sessionId])
|
||||||
@@index([website_id])
|
@@index([websiteId])
|
||||||
|
@@index([eventUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model event_data {
|
model eventData {
|
||||||
event_data_id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement()) @map("event_data_id")
|
||||||
event_id Int @unique
|
eventId Int @unique @map("event_id")
|
||||||
event_data Json
|
eventData Json @map("event_data")
|
||||||
event event @relation(fields: [event_id], references: [event_id])
|
event event @relation(fields: [eventId], references: [id])
|
||||||
|
|
||||||
|
@@map("event_data")
|
||||||
}
|
}
|
||||||
|
|
||||||
model pageview {
|
model pageview {
|
||||||
view_id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement()) @map("view_id")
|
||||||
website_id Int
|
websiteId Int @map("website_id")
|
||||||
session_id Int
|
sessionId Int @map("session_id")
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @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])
|
session session @relation(fields: [sessionId], references: [id])
|
||||||
website website @relation(fields: [website_id], references: [website_id])
|
website website @relation(fields: [websiteId], references: [id])
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([createdAt])
|
||||||
@@index([session_id])
|
@@index([sessionId])
|
||||||
@@index([website_id, created_at])
|
@@index([websiteId, createdAt])
|
||||||
@@index([website_id])
|
@@index([websiteId])
|
||||||
@@index([website_id, session_id, created_at])
|
@@index([websiteId, sessionId, createdAt])
|
||||||
}
|
}
|
||||||
|
|
||||||
model session {
|
model session {
|
||||||
session_id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement()) @map("session_id")
|
||||||
session_uuid String @unique @db.Uuid
|
sessionUuid String @unique @map("session_uuid") @db.Uuid
|
||||||
website_id Int
|
websiteId Int @map("website_id")
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
hostname String? @db.VarChar(100)
|
hostname String? @db.VarChar(100)
|
||||||
browser String? @db.VarChar(20)
|
browser String? @db.VarChar(20)
|
||||||
os String? @db.VarChar(20)
|
os String? @db.VarChar(20)
|
||||||
|
@ -69,26 +76,28 @@ 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])
|
website website? @relation(fields: [websiteId], references: [id])
|
||||||
event event[]
|
events event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
|
|
||||||
@@index([created_at])
|
@@index([createdAt])
|
||||||
@@index([website_id])
|
@@index([websiteId])
|
||||||
|
@@index([sessionUuid])
|
||||||
}
|
}
|
||||||
|
|
||||||
model website {
|
model website {
|
||||||
website_id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement()) @map("website_id")
|
||||||
website_uuid String @unique @db.Uuid
|
websiteUuid String @unique @map("website_uuid") @db.Uuid
|
||||||
user_id Int
|
userId Int @map("user_id")
|
||||||
name String @db.VarChar(100)
|
name String @db.VarChar(100)
|
||||||
domain String? @db.VarChar(500)
|
domain String? @db.VarChar(500)
|
||||||
share_id String? @unique @db.VarChar(64)
|
shareId String? @unique @map("share_id") @db.VarChar(64)
|
||||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||||
account account @relation(fields: [user_id], references: [user_id])
|
account account @relation(fields: [userId], references: [id])
|
||||||
event event[]
|
event event[]
|
||||||
pageview pageview[]
|
pageview pageview[]
|
||||||
session session[]
|
session session[]
|
||||||
|
|
||||||
@@index([user_id])
|
@@index([userId])
|
||||||
|
@@index([websiteUuid])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
{
|
{
|
||||||
"label.accounts": "Accounts",
|
"label.accounts": "Accounts",
|
||||||
"label.add-account": "Add account",
|
"label.add-account": "Add account",
|
||||||
|
"label.add-column": "Add column",
|
||||||
|
"label.add-filter": "Add filter",
|
||||||
"label.add-website": "Add website",
|
"label.add-website": "Add website",
|
||||||
"label.administrator": "Administrator",
|
"label.administrator": "Administrator",
|
||||||
"label.all": "All",
|
"label.all": "All",
|
||||||
|
@ -25,6 +27,8 @@
|
||||||
"label.edit-account": "Edit account",
|
"label.edit-account": "Edit account",
|
||||||
"label.edit-website": "Edit website",
|
"label.edit-website": "Edit website",
|
||||||
"label.enable-share-url": "Enable share URL",
|
"label.enable-share-url": "Enable share URL",
|
||||||
|
"label.event-data": "Event Data",
|
||||||
|
"label.field-name": "Field Name",
|
||||||
"label.invalid": "Invalid",
|
"label.invalid": "Invalid",
|
||||||
"label.invalid-domain": "Invalid domain",
|
"label.invalid-domain": "Invalid domain",
|
||||||
"label.language": "Language",
|
"label.language": "Language",
|
||||||
|
@ -48,6 +52,7 @@
|
||||||
"label.reset": "Reset",
|
"label.reset": "Reset",
|
||||||
"label.reset-website": "Reset statistics",
|
"label.reset-website": "Reset statistics",
|
||||||
"label.save": "Save",
|
"label.save": "Save",
|
||||||
|
"label.search": "Search",
|
||||||
"label.settings": "Settings",
|
"label.settings": "Settings",
|
||||||
"label.share-url": "Share URL",
|
"label.share-url": "Share URL",
|
||||||
"label.single-day": "Single day",
|
"label.single-day": "Single day",
|
||||||
|
@ -58,8 +63,10 @@
|
||||||
"label.timezone": "Timezone",
|
"label.timezone": "Timezone",
|
||||||
"label.today": "Today",
|
"label.today": "Today",
|
||||||
"label.tracking-code": "Tracking code",
|
"label.tracking-code": "Tracking code",
|
||||||
|
"label.type": "Type",
|
||||||
"label.unknown": "Unknown",
|
"label.unknown": "Unknown",
|
||||||
"label.username": "Username",
|
"label.username": "Username",
|
||||||
|
"label.value": "Value",
|
||||||
"label.view-details": "View details",
|
"label.view-details": "View details",
|
||||||
"label.websites": "Websites",
|
"label.websites": "Websites",
|
||||||
"label.yesterday": "Yesterday",
|
"label.yesterday": "Yesterday",
|
||||||
|
|
|
@ -62,6 +62,7 @@
|
||||||
"label.username": "用户名",
|
"label.username": "用户名",
|
||||||
"label.view-details": "查看更多",
|
"label.view-details": "查看更多",
|
||||||
"label.websites": "网站",
|
"label.websites": "网站",
|
||||||
|
"label.yesterday": "昨天",
|
||||||
"message.active-users": "当前在线 {x} 人",
|
"message.active-users": "当前在线 {x} 人",
|
||||||
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
"message.confirm-delete": "你确定要删除 {target} 吗?",
|
||||||
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
|
"message.confirm-reset": "您确定要重置 {target} 的数据吗?",
|
||||||
|
|
48
lib/auth.js
48
lib/auth.js
|
@ -1,9 +1,9 @@
|
||||||
import { parseSecureToken, parseToken, getItem } from 'next-basics';
|
import { parseSecureToken, parseToken } from 'next-basics';
|
||||||
import { AUTH_TOKEN, SHARE_TOKEN_HEADER } from './constants';
|
import { getWebsite } from 'queries';
|
||||||
import { getWebsiteById } from 'queries';
|
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||||
import { secret } from './crypto';
|
import { secret } from 'lib/crypto';
|
||||||
|
|
||||||
export async function getAuthToken(req) {
|
export function getAuthToken(req) {
|
||||||
try {
|
try {
|
||||||
const token = req.headers.authorization;
|
const token = req.headers.authorization;
|
||||||
|
|
||||||
|
@ -13,13 +13,15 @@ export async function getAuthToken(req) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getAuthHeader() {
|
export function getShareToken(req) {
|
||||||
const token = getItem(AUTH_TOKEN);
|
try {
|
||||||
|
return parseSecureToken(req.headers[SHARE_TOKEN_HEADER], secret());
|
||||||
return token ? { authorization: `Bearer ${token}` } : {};
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function isValidToken(token, validation) {
|
export function isValidToken(token, validation) {
|
||||||
try {
|
try {
|
||||||
const result = parseToken(token, secret());
|
const result = parseToken(token, secret());
|
||||||
|
|
||||||
|
@ -35,25 +37,23 @@ export async function isValidToken(token, validation) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function allowQuery(req, skipToken) {
|
export async function allowQuery(req) {
|
||||||
const { id } = req.query;
|
const { id: websiteId } = req.query;
|
||||||
const token = req.headers[SHARE_TOKEN_HEADER];
|
|
||||||
const websiteId = +id;
|
|
||||||
|
|
||||||
const website = await getWebsiteById(websiteId);
|
const { userId, isAdmin, shareToken } = req.auth ?? {};
|
||||||
|
|
||||||
if (website) {
|
if (isAdmin) {
|
||||||
if (token && token !== 'undefined' && !skipToken) {
|
return true;
|
||||||
return isValidToken(token, { website_id: websiteId });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const authToken = await getAuthToken(req);
|
if (shareToken) {
|
||||||
|
return isValidToken(shareToken, { websiteUuid: websiteId });
|
||||||
if (authToken) {
|
|
||||||
const { user_id, is_admin } = authToken;
|
|
||||||
|
|
||||||
return is_admin || website.user_id === user_id;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
const website = await getWebsite({ websiteUuid: websiteId });
|
||||||
|
|
||||||
|
return website && website.userId === userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
|
@ -14,6 +14,9 @@ export const CLICKHOUSE_DATE_FORMATS = {
|
||||||
|
|
||||||
const log = debug('umami:clickhouse');
|
const log = debug('umami:clickhouse');
|
||||||
|
|
||||||
|
let clickhouse;
|
||||||
|
const enabled = Boolean(process.env.CLICKHOUSE_URL);
|
||||||
|
|
||||||
function getClient() {
|
function getClient() {
|
||||||
const {
|
const {
|
||||||
hostname,
|
hostname,
|
||||||
|
@ -57,12 +60,53 @@ function getDateFormat(date) {
|
||||||
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBetweenDates(field, start_at, end_at) {
|
function getCommaSeparatedStringFormat(data) {
|
||||||
return `${field} between ${getDateFormat(start_at)}
|
return data.map(a => `'${a}'`).join(',') || '';
|
||||||
and ${getDateFormat(end_at)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFilterQuery(table, column, filters = {}, params = []) {
|
function getBetweenDates(field, start_at, end_at) {
|
||||||
|
return `${field} between ${getDateFormat(start_at)} and ${getDateFormat(end_at)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getJsonField(column, property) {
|
||||||
|
return `${column}.${property}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataColumnsQuery(column, columns) {
|
||||||
|
const query = Object.keys(columns).reduce((arr, key) => {
|
||||||
|
const filter = columns[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.push(`${filter}(${getJsonField(column, key)}) as "${filter}(${key})"`);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join(',\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataFilterQuery(column, filters) {
|
||||||
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
|
const filter = filters[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
arr.push(
|
||||||
|
`${getJsonField(column, key)} = ${typeof filter === 'string' ? `'${filter}'` : filter}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join('\nand ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getFilterQuery(column, filters = {}, params = []) {
|
||||||
const query = Object.keys(filters).reduce((arr, key) => {
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
const filter = filters[key];
|
const filter = filters[key];
|
||||||
|
|
||||||
|
@ -72,48 +116,28 @@ function getFilterQuery(table, column, filters = {}, params = []) {
|
||||||
|
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'url':
|
case 'url':
|
||||||
if (table === 'pageview' || table === 'event') {
|
|
||||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
|
||||||
params.push(decodeURIComponent(filter));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'os':
|
case 'os':
|
||||||
case 'browser':
|
case 'browser':
|
||||||
case 'device':
|
case 'device':
|
||||||
case 'country':
|
case 'country':
|
||||||
if (table === 'session') {
|
|
||||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
|
||||||
params.push(decodeURIComponent(filter));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'event_name':
|
case 'event_name':
|
||||||
if (table === 'event') {
|
arr.push(`and ${key}=$${params.length + 1}`);
|
||||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
|
||||||
params.push(decodeURIComponent(filter));
|
params.push(decodeURIComponent(filter));
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'referrer':
|
case 'referrer':
|
||||||
if (table === 'pageview' || table === 'event') {
|
arr.push(`and referrer like $${params.length + 1}`);
|
||||||
arr.push(`and ${table}.referrer like $${params.length + 1}`);
|
|
||||||
params.push(`%${decodeURIComponent(filter)}%`);
|
params.push(`%${decodeURIComponent(filter)}%`);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'domain':
|
case 'domain':
|
||||||
if (table === 'pageview') {
|
arr.push(`and referrer not like $${params.length + 1}`);
|
||||||
arr.push(`and ${table}.referrer not like $${params.length + 1}`);
|
arr.push(`and referrer not like '/%'`);
|
||||||
arr.push(`and ${table}.referrer not like '/%'`);
|
|
||||||
params.push(`%://${filter}/%`);
|
params.push(`%://${filter}/%`);
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'query':
|
case 'query':
|
||||||
if (table === 'pageview') {
|
arr.push(`and url like '%?%'`);
|
||||||
arr.push(`and ${table}.url like '%?%'`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return arr;
|
return arr;
|
||||||
|
@ -122,7 +146,7 @@ function getFilterQuery(table, column, filters = {}, params = []) {
|
||||||
return query.join('\n');
|
return query.join('\n');
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFilters(table, column, filters = {}, params = [], sessionKey = 'session_id') {
|
function parseFilters(column, filters = {}, params = []) {
|
||||||
const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } =
|
const { domain, url, event_url, referrer, os, browser, device, country, event_name, query } =
|
||||||
filters;
|
filters;
|
||||||
|
|
||||||
|
@ -135,13 +159,9 @@ function parseFilters(table, column, filters = {}, params = [], sessionKey = 'se
|
||||||
sessionFilters,
|
sessionFilters,
|
||||||
eventFilters,
|
eventFilters,
|
||||||
event: { event_name },
|
event: { event_name },
|
||||||
joinSession:
|
pageviewQuery: getFilterQuery(column, pageviewFilters, params),
|
||||||
os || browser || device || country
|
sessionQuery: getFilterQuery(column, sessionFilters, params),
|
||||||
? `inner join session on ${table}.${sessionKey} = session.${sessionKey}`
|
eventQuery: getFilterQuery(column, eventFilters, params),
|
||||||
: '',
|
|
||||||
pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params),
|
|
||||||
sessionQuery: getFilterQuery('session', column, sessionFilters, params),
|
|
||||||
eventQuery: getFilterQuery('event', column, eventFilters, params),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -168,6 +188,8 @@ async function rawQuery(query, params = []) {
|
||||||
log(formattedQuery);
|
log(formattedQuery);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await connect();
|
||||||
|
|
||||||
return clickhouse.query(formattedQuery).toPromise();
|
return clickhouse.query(formattedQuery).toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,16 +205,26 @@ async function findFirst(data) {
|
||||||
return data[0] ?? null;
|
return data[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization
|
async function connect() {
|
||||||
const clickhouse = process.env.CLICKHOUSE_URL && (global[CLICKHOUSE] || getClient());
|
if (!clickhouse) {
|
||||||
|
clickhouse = process.env.CLICKHOUSE_URL && (global[CLICKHOUSE] || getClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
return clickhouse;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
enabled,
|
||||||
client: clickhouse,
|
client: clickhouse,
|
||||||
log,
|
log,
|
||||||
|
connect,
|
||||||
getDateStringQuery,
|
getDateStringQuery,
|
||||||
getDateQuery,
|
getDateQuery,
|
||||||
getDateFormat,
|
getDateFormat,
|
||||||
|
getCommaSeparatedStringFormat,
|
||||||
getBetweenDates,
|
getBetweenDates,
|
||||||
|
getEventDataColumnsQuery,
|
||||||
|
getEventDataFilterQuery,
|
||||||
getFilterQuery,
|
getFilterQuery,
|
||||||
parseFilters,
|
parseFilters,
|
||||||
findUnique,
|
findUnique,
|
||||||
|
|
|
@ -12,7 +12,7 @@ BigInt.prototype.toJSON = function () {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getDatabaseType(url = process.env.DATABASE_URL) {
|
export function getDatabaseType(url = process.env.DATABASE_URL) {
|
||||||
const type = process.env.DATABASE_TYPE || (url && url.split(':')[0]);
|
const type = url && url.split(':')[0];
|
||||||
|
|
||||||
if (type === 'postgres') {
|
if (type === 'postgres') {
|
||||||
return POSTGRESQL;
|
return POSTGRESQL;
|
||||||
|
|
|
@ -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)).substring(-2);
|
color += ('00' + value.toString(16)).slice(-2);
|
||||||
}
|
}
|
||||||
return color;
|
return color;
|
||||||
}
|
}
|
||||||
|
|
35
lib/kafka.js
35
lib/kafka.js
|
@ -5,6 +5,10 @@ import { KAFKA, KAFKA_PRODUCER } from 'lib/db';
|
||||||
|
|
||||||
const log = debug('umami:kafka');
|
const log = debug('umami:kafka');
|
||||||
|
|
||||||
|
let kafka;
|
||||||
|
let producer;
|
||||||
|
const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER);
|
||||||
|
|
||||||
function getClient() {
|
function getClient() {
|
||||||
const { username, password } = new URL(process.env.KAFKA_URL);
|
const { username, password } = new URL(process.env.KAFKA_URL);
|
||||||
const brokers = process.env.KAFKA_BROKER.split(',');
|
const brokers = process.env.KAFKA_BROKER.split(',');
|
||||||
|
@ -12,7 +16,12 @@ function getClient() {
|
||||||
const ssl =
|
const ssl =
|
||||||
username && password
|
username && password
|
||||||
? {
|
? {
|
||||||
ssl: true,
|
ssl: {
|
||||||
|
checkServerIdentity: () => undefined,
|
||||||
|
ca: [process.env.CA_CERT],
|
||||||
|
key: process.env.CLIENT_KEY,
|
||||||
|
cert: process.env.CLIENT_CERT,
|
||||||
|
},
|
||||||
sasl: {
|
sasl: {
|
||||||
mechanism: 'plain',
|
mechanism: 'plain',
|
||||||
username,
|
username,
|
||||||
|
@ -33,6 +42,8 @@ function getClient() {
|
||||||
global[KAFKA] = client;
|
global[KAFKA] = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log('Kafka initialized');
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +55,8 @@ async function getProducer() {
|
||||||
global[KAFKA_PRODUCER] = producer;
|
global[KAFKA_PRODUCER] = producer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log('Kafka producer initialized');
|
||||||
|
|
||||||
return producer;
|
return producer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,6 +65,8 @@ function getDateFormat(date) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendMessage(params, topic) {
|
async function sendMessage(params, topic) {
|
||||||
|
await connect();
|
||||||
|
|
||||||
await producer.send({
|
await producer.send({
|
||||||
topic,
|
topic,
|
||||||
messages: [
|
messages: [
|
||||||
|
@ -59,26 +74,28 @@ async function sendMessage(params, topic) {
|
||||||
value: JSON.stringify(params),
|
value: JSON.stringify(params),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
acks: 0,
|
acks: 1,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization
|
async function connect() {
|
||||||
let kafka;
|
if (!kafka) {
|
||||||
let producer;
|
|
||||||
|
|
||||||
(async () => {
|
|
||||||
kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (global[KAFKA] || getClient());
|
kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (global[KAFKA] || getClient());
|
||||||
|
|
||||||
if (kafka) {
|
if (kafka) {
|
||||||
producer = global[KAFKA_PRODUCER] || (await getProducer());
|
producer = global[KAFKA_PRODUCER] || (await getProducer());
|
||||||
}
|
}
|
||||||
})();
|
}
|
||||||
|
|
||||||
|
return kafka;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
enabled,
|
||||||
client: kafka,
|
client: kafka,
|
||||||
producer: producer,
|
producer,
|
||||||
log,
|
log,
|
||||||
|
connect,
|
||||||
getDateFormat,
|
getDateFormat,
|
||||||
sendMessage,
|
sendMessage,
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { createMiddleware, unauthorized, badRequest, serverError } from 'next-basics';
|
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, getShareToken } from './auth';
|
||||||
|
|
||||||
export const useCors = createMiddleware(cors());
|
export const useCors = createMiddleware(cors());
|
||||||
|
|
||||||
|
@ -27,11 +27,12 @@ export const useSession = createMiddleware(async (req, res, next) => {
|
||||||
|
|
||||||
export const useAuth = createMiddleware(async (req, res, next) => {
|
export const useAuth = createMiddleware(async (req, res, next) => {
|
||||||
const token = await getAuthToken(req);
|
const token = await getAuthToken(req);
|
||||||
|
const shareToken = await getShareToken(req);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
req.auth = token;
|
req.auth = { ...token, shareToken };
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
|
@ -85,6 +85,64 @@ function getTimestampInterval(field) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getJsonField(column, property, isNumber) {
|
||||||
|
const db = getDatabaseType(process.env.DATABASE_URL);
|
||||||
|
|
||||||
|
if (db === POSTGRESQL) {
|
||||||
|
let accessor = `${column} ->> '${property}'`;
|
||||||
|
|
||||||
|
if (isNumber) {
|
||||||
|
accessor = `CAST(${accessor} AS DECIMAL)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (db === MYSQL) {
|
||||||
|
return `${column} ->> "$.${property}"`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataColumnsQuery(column, columns) {
|
||||||
|
const query = Object.keys(columns).reduce((arr, key) => {
|
||||||
|
const filter = columns[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumber = ['sum', 'avg', 'min', 'max'].some(a => a === filter);
|
||||||
|
|
||||||
|
arr.push(`${filter}(${getJsonField(column, key, isNumber)}) as "${filter}(${key})"`);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join(',\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEventDataFilterQuery(column, filters) {
|
||||||
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
|
const filter = filters[key];
|
||||||
|
|
||||||
|
if (filter === undefined) {
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isNumber = filter && typeof filter === 'number';
|
||||||
|
|
||||||
|
arr.push(
|
||||||
|
`${getJsonField(column, key, isNumber)} = ${
|
||||||
|
typeof filter === 'string' ? `'${filter}'` : filter
|
||||||
|
}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return query.join('\nand ');
|
||||||
|
}
|
||||||
|
|
||||||
function getFilterQuery(table, column, filters = {}, params = []) {
|
function getFilterQuery(table, column, filters = {}, params = []) {
|
||||||
const query = Object.keys(filters).reduce((arr, key) => {
|
const query = Object.keys(filters).reduce((arr, key) => {
|
||||||
const filter = filters[key];
|
const filter = filters[key];
|
||||||
|
@ -193,6 +251,8 @@ export default {
|
||||||
getDateQuery,
|
getDateQuery,
|
||||||
getTimestampInterval,
|
getTimestampInterval,
|
||||||
getFilterQuery,
|
getFilterQuery,
|
||||||
|
getEventDataColumnsQuery,
|
||||||
|
getEventDataFilterQuery,
|
||||||
parseFilters,
|
parseFilters,
|
||||||
rawQuery,
|
rawQuery,
|
||||||
transaction,
|
transaction,
|
||||||
|
|
45
lib/redis.js
45
lib/redis.js
|
@ -8,12 +8,20 @@ const log = debug('umami:redis');
|
||||||
const INITIALIZED = 'redis:initialized';
|
const INITIALIZED = 'redis:initialized';
|
||||||
export const DELETED = 'deleted';
|
export const DELETED = 'deleted';
|
||||||
|
|
||||||
|
let redis;
|
||||||
|
const enabled = Boolean(process.env.REDIS_URL);
|
||||||
|
|
||||||
function getClient() {
|
function getClient() {
|
||||||
if (!process.env.REDIS_URL) {
|
if (!process.env.REDIS_URL) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const redis = new Redis(process.env.REDIS_URL);
|
const redis = new Redis(process.env.REDIS_URL, {
|
||||||
|
retryStrategy(times) {
|
||||||
|
log(`Redis reconnecting attempt: ${times}`);
|
||||||
|
return 5000;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
global[REDIS] = redis;
|
global[REDIS] = redis;
|
||||||
|
@ -29,32 +37,43 @@ async function stageData() {
|
||||||
const websites = await getAllWebsites();
|
const websites = await getAllWebsites();
|
||||||
|
|
||||||
const sessionUuids = sessions.map(a => {
|
const sessionUuids = sessions.map(a => {
|
||||||
return { key: `session:${a.session_uuid}`, value: 1 };
|
return { key: `session:${a.sessionUuid}`, value: 1 };
|
||||||
});
|
});
|
||||||
const websiteIds = websites.map(a => {
|
const websiteIds = websites.map(a => {
|
||||||
return { key: `website:${a.website_uuid}`, value: Number(a.website_id) };
|
return { key: `website:${a.websiteUuid}`, value: Number(a.websiteId) };
|
||||||
});
|
});
|
||||||
|
|
||||||
await addRedis(sessionUuids);
|
await addSet(sessionUuids);
|
||||||
await addRedis(websiteIds);
|
await addSet(websiteIds);
|
||||||
|
|
||||||
await redis.set(INITIALIZED, 1);
|
await redis.set(INITIALIZED, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addRedis(ids) {
|
async function addSet(ids) {
|
||||||
for (let i = 0; i < ids.length; i++) {
|
for (let i = 0; i < ids.length; i++) {
|
||||||
const { key, value } = ids[i];
|
const { key, value } = ids[i];
|
||||||
await redis.set(key, value);
|
await redis.set(key, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialization
|
async function get(key) {
|
||||||
const redis = process.env.REDIS_URL && (global[REDIS] || getClient());
|
await connect();
|
||||||
|
|
||||||
(async () => {
|
return redis.get(key);
|
||||||
if (redis && !(await redis.get(INITIALIZED))) {
|
|
||||||
await stageData();
|
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
|
||||||
export default { client: redis, stageData, log };
|
async function set(key, value) {
|
||||||
|
await connect();
|
||||||
|
|
||||||
|
return redis.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connect() {
|
||||||
|
if (!redis) {
|
||||||
|
redis = process.env.REDIS_URL && (global[REDIS] || getClient());
|
||||||
|
}
|
||||||
|
|
||||||
|
return redis;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default { enabled, client: redis, log, connect, get, set, stageData };
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
import { parseToken } from 'next-basics';
|
import { parseToken } from 'next-basics';
|
||||||
import { validate } from 'uuid';
|
import { validate } from 'uuid';
|
||||||
import { uuid } from 'lib/crypto';
|
import { secret, uuid } from 'lib/crypto';
|
||||||
import redis, { DELETED } from 'lib/redis';
|
import redis, { DELETED } from 'lib/redis';
|
||||||
|
import clickhouse from 'lib/clickhouse';
|
||||||
import { getClientInfo, getJsonBody } from 'lib/request';
|
import { getClientInfo, getJsonBody } from 'lib/request';
|
||||||
import { createSession, getSessionByUuid, getWebsiteByUuid } from 'queries';
|
import { createSession, getSessionByUuid, getWebsiteByUuid } from 'queries';
|
||||||
|
|
||||||
|
@ -15,60 +16,58 @@ export async function getSession(req) {
|
||||||
const cache = req.headers['x-umami-cache'];
|
const cache = req.headers['x-umami-cache'];
|
||||||
|
|
||||||
if (cache) {
|
if (cache) {
|
||||||
const result = await parseToken(cache);
|
const result = await parseToken(cache, secret());
|
||||||
|
|
||||||
if (result) {
|
if (result) {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { website: website_uuid, hostname, screen, language } = payload;
|
const { website: websiteUuid, hostname, screen, language } = payload;
|
||||||
|
|
||||||
if (!validate(website_uuid)) {
|
if (!validate(websiteUuid)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let websiteId = null;
|
let websiteId = null;
|
||||||
|
|
||||||
// Check if website exists
|
// Check if website exists
|
||||||
if (redis.client) {
|
if (redis.enabled) {
|
||||||
websiteId = await redis.client.get(`website:${website_uuid}`);
|
websiteId = Number(await redis.get(`website:${websiteUuid}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check database if redis does not have
|
// Check database if does not exists in Redis
|
||||||
if (!websiteId) {
|
if (!websiteId) {
|
||||||
const website = await getWebsiteByUuid(website_uuid);
|
const website = await getWebsiteByUuid(websiteUuid);
|
||||||
websiteId = website ? website.website_id : null;
|
websiteId = website ? website.id : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!websiteId || websiteId === DELETED) {
|
if (!websiteId || websiteId === DELETED) {
|
||||||
throw new Error(`Website not found: ${website_uuid}`);
|
throw new Error(`Website not found: ${websiteUuid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userAgent, browser, os, ip, country, device } = await getClientInfo(req, payload);
|
const { userAgent, browser, os, ip, country, device } = await getClientInfo(req, payload);
|
||||||
|
const sessionUuid = uuid(websiteUuid, hostname, ip, userAgent);
|
||||||
|
|
||||||
const session_uuid = uuid(websiteId, hostname, ip, userAgent);
|
|
||||||
|
|
||||||
let sessionCreated = false;
|
|
||||||
let sessionId = null;
|
let sessionId = null;
|
||||||
let session = null;
|
let session = null;
|
||||||
|
|
||||||
|
if (!clickhouse.enabled) {
|
||||||
// Check if session exists
|
// Check if session exists
|
||||||
if (redis.client) {
|
if (redis.enabled) {
|
||||||
sessionCreated = !!(await redis.client.get(`session:${session_uuid}`));
|
sessionId = Number(await redis.get(`session:${sessionUuid}`));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check database if redis does not have
|
// Check database if does not exists in Redis
|
||||||
if (!sessionCreated) {
|
if (!sessionId) {
|
||||||
session = await getSessionByUuid(session_uuid);
|
session = await getSessionByUuid(sessionUuid);
|
||||||
sessionCreated = !!session;
|
sessionId = session ? session.id : null;
|
||||||
sessionId = session ? session.session_id : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sessionCreated) {
|
if (!sessionId) {
|
||||||
try {
|
try {
|
||||||
session = await createSession(websiteId, {
|
session = await createSession(websiteId, {
|
||||||
session_uuid,
|
sessionUuid,
|
||||||
hostname,
|
hostname,
|
||||||
browser,
|
browser,
|
||||||
os,
|
os,
|
||||||
|
@ -77,18 +76,31 @@ export async function getSession(req) {
|
||||||
country,
|
country,
|
||||||
device,
|
device,
|
||||||
});
|
});
|
||||||
|
|
||||||
sessionId = session ? session.session_id : null;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!e.message.toLowerCase().includes('unique constraint')) {
|
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||||
throw e;
|
throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
return {
|
session = {
|
||||||
website_id: websiteId,
|
sessionId,
|
||||||
session_id: sessionId,
|
sessionUuid,
|
||||||
session_uuid,
|
hostname,
|
||||||
|
browser,
|
||||||
|
os,
|
||||||
|
screen,
|
||||||
|
language,
|
||||||
|
country,
|
||||||
|
device,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
website: {
|
||||||
|
websiteId,
|
||||||
|
websiteUuid,
|
||||||
|
},
|
||||||
|
session,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,6 +36,7 @@ module.exports = {
|
||||||
env: {
|
env: {
|
||||||
currentVersion: pkg.version,
|
currentVersion: pkg.version,
|
||||||
isProduction: process.env.NODE_ENV === 'production',
|
isProduction: process.env.NODE_ENV === 'production',
|
||||||
|
isCloudMode: process.env.CLOUD_MODE,
|
||||||
},
|
},
|
||||||
basePath: process.env.BASE_PATH,
|
basePath: process.env.BASE_PATH,
|
||||||
output: 'standalone',
|
output: 'standalone',
|
||||||
|
|
10
package.json
10
package.json
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "umami",
|
"name": "umami",
|
||||||
"version": "1.38.0",
|
"version": "1.39.0",
|
||||||
"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",
|
||||||
|
@ -56,7 +56,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/inter": "4.5.7",
|
"@fontsource/inter": "4.5.7",
|
||||||
"@prisma/client": "4.3.1",
|
"@prisma/client": "4.5.0",
|
||||||
"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",
|
||||||
|
@ -83,8 +83,8 @@
|
||||||
"kafkajs": "^2.1.0",
|
"kafkajs": "^2.1.0",
|
||||||
"maxmind": "^4.3.6",
|
"maxmind": "^4.3.6",
|
||||||
"moment-timezone": "^0.5.35",
|
"moment-timezone": "^0.5.35",
|
||||||
"next": "^12.2.5",
|
"next": "^12.3.1",
|
||||||
"next-basics": "^0.7.0",
|
"next-basics": "^0.18.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",
|
||||||
|
@ -125,7 +125,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.3.1",
|
"prisma": "4.5.0",
|
||||||
"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,29 +0,0 @@
|
||||||
import { getAccountById, deleteAccount } from 'queries';
|
|
||||||
import { useAuth } from 'lib/middleware';
|
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
await useAuth(req, res);
|
|
||||||
|
|
||||||
const { is_admin } = req.auth;
|
|
||||||
const { id } = req.query;
|
|
||||||
const user_id = +id;
|
|
||||||
|
|
||||||
if (!is_admin) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
const account = await getAccountById(user_id);
|
|
||||||
|
|
||||||
return ok(res, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'DELETE') {
|
|
||||||
await deleteAccount(user_id);
|
|
||||||
|
|
||||||
return ok(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
|
||||||
};
|
|
|
@ -1,57 +0,0 @@
|
||||||
import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics';
|
|
||||||
import { getAccountById, getAccountByUsername, updateAccount, createAccount } from 'queries';
|
|
||||||
import { useAuth } from 'lib/middleware';
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
await useAuth(req, res);
|
|
||||||
|
|
||||||
const { user_id: current_user_id, is_admin: current_user_is_admin } = req.auth;
|
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const { user_id, username, password, is_admin } = req.body;
|
|
||||||
|
|
||||||
if (user_id) {
|
|
||||||
const account = await getAccountById(user_id);
|
|
||||||
|
|
||||||
if (account.user_id === current_user_id || current_user_is_admin) {
|
|
||||||
const data = {};
|
|
||||||
|
|
||||||
if (password) {
|
|
||||||
data.password = hashPassword(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only admin can change these fields
|
|
||||||
if (current_user_is_admin) {
|
|
||||||
data.username = username;
|
|
||||||
data.is_admin = is_admin;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.username && account.username !== data.username) {
|
|
||||||
const accountByUsername = await getAccountByUsername(username);
|
|
||||||
|
|
||||||
if (accountByUsername) {
|
|
||||||
return badRequest(res, 'Account already exists');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updated = await updateAccount(user_id, data);
|
|
||||||
|
|
||||||
return ok(res, updated);
|
|
||||||
}
|
|
||||||
|
|
||||||
return unauthorized(res);
|
|
||||||
} else {
|
|
||||||
const accountByUsername = await getAccountByUsername(username);
|
|
||||||
|
|
||||||
if (accountByUsername) {
|
|
||||||
return badRequest(res, 'Account already exists');
|
|
||||||
}
|
|
||||||
|
|
||||||
const created = await createAccount({ username, password: hashPassword(password) });
|
|
||||||
|
|
||||||
return ok(res, created);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
|
||||||
};
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
|
import { getAccount, deleteAccount, updateAccount } from 'queries';
|
||||||
|
import { useAuth } from 'lib/middleware';
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
const { isAdmin, userId } = req.auth;
|
||||||
|
const { id } = req.query;
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
if (id !== userId && !isAdmin) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await getAccount({ id: +id });
|
||||||
|
|
||||||
|
return ok(res, account);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const { username, password } = req.body;
|
||||||
|
|
||||||
|
if (id !== userId && !isAdmin) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const account = await getAccount({ id: +id });
|
||||||
|
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
if (password) {
|
||||||
|
data.password = hashPassword(password);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only admin can change these fields
|
||||||
|
if (isAdmin) {
|
||||||
|
data.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check when username changes
|
||||||
|
if (data.username && account.username !== data.username) {
|
||||||
|
const accountByUsername = await getAccount({ username });
|
||||||
|
|
||||||
|
if (accountByUsername) {
|
||||||
|
return badRequest(res, 'Account already exists');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await updateAccount(data, { id: +id });
|
||||||
|
|
||||||
|
return ok(res, updated);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
if (!isAdmin) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteAccount(userId);
|
||||||
|
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
|
@ -12,24 +12,25 @@ import {
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
const { user_id: auth_user_id, is_admin } = req.auth;
|
const { userId: currentUserId, isAdmin: currentUserIsAdmin } = req.auth;
|
||||||
const { user_id, current_password, new_password } = req.body;
|
const { current_password, new_password } = req.body;
|
||||||
|
const { id } = req.query;
|
||||||
|
const userId = +id;
|
||||||
|
|
||||||
if (!is_admin && user_id !== auth_user_id) {
|
if (!currentUserIsAdmin && userId !== currentUserId) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
const account = await getAccountById(user_id);
|
const account = await getAccountById(userId);
|
||||||
const valid = checkPassword(current_password, account.password);
|
|
||||||
|
|
||||||
if (!valid) {
|
if (!checkPassword(current_password, account.password)) {
|
||||||
return badRequest(res, 'Current password is incorrect');
|
return badRequest(res, 'Current password is incorrect');
|
||||||
}
|
}
|
||||||
|
|
||||||
const password = hashPassword(new_password);
|
const password = hashPassword(new_password);
|
||||||
|
|
||||||
const updated = await updateAccount(user_id, { password });
|
const updated = await updateAccount(userId, { password });
|
||||||
|
|
||||||
return ok(res, updated);
|
return ok(res, updated);
|
||||||
}
|
}
|
|
@ -1,13 +1,14 @@
|
||||||
import { getAccounts } from 'queries';
|
import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, unauthorized, methodNotAllowed } from 'next-basics';
|
import { uuid } from 'lib/crypto';
|
||||||
|
import { createAccount, getAccountByUsername, getAccounts } from 'queries';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
const { is_admin } = req.auth;
|
const { isAdmin } = req.auth;
|
||||||
|
|
||||||
if (!is_admin) {
|
if (!isAdmin) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -17,5 +18,23 @@ export default async (req, res) => {
|
||||||
return ok(res, accounts);
|
return ok(res, accounts);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const { username, password, account_uuid } = req.body;
|
||||||
|
|
||||||
|
const accountByUsername = await getAccountByUsername(username);
|
||||||
|
|
||||||
|
if (accountByUsername) {
|
||||||
|
return badRequest(res, 'Account already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await createAccount({
|
||||||
|
username,
|
||||||
|
password: hashPassword(password),
|
||||||
|
accountUuid: account_uuid || uuid(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok(res, created);
|
||||||
|
}
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
return methodNotAllowed(res);
|
||||||
};
|
};
|
||||||
|
|
|
@ -12,8 +12,8 @@ export default async (req, res) => {
|
||||||
const account = await getAccountByUsername(username);
|
const account = await getAccountByUsername(username);
|
||||||
|
|
||||||
if (account && checkPassword(password, account.password)) {
|
if (account && checkPassword(password, account.password)) {
|
||||||
const { user_id, username, is_admin } = account;
|
const { id, username, isAdmin, accountUuid } = account;
|
||||||
const user = { user_id, username, is_admin };
|
const user = { userId: id, username, isAdmin, accountUuid };
|
||||||
const token = createSecureToken(user, secret());
|
const token = createSecureToken(user, secret());
|
||||||
|
|
||||||
return ok(res, { token, user });
|
return ok(res, { token, user });
|
||||||
|
|
|
@ -58,36 +58,39 @@ export default async (req, res) => {
|
||||||
|
|
||||||
await useSession(req, res);
|
await useSession(req, res);
|
||||||
|
|
||||||
const {
|
const { website, session } = req.session;
|
||||||
session: { website_id, session_id, session_uuid },
|
|
||||||
} = req;
|
|
||||||
|
|
||||||
const { type, payload } = getJsonBody(req);
|
const { type, payload } = getJsonBody(req);
|
||||||
|
|
||||||
let { url, referrer, event_name, event_data } = payload;
|
let { url, referrer, event_name: eventName, event_data: eventData } = payload;
|
||||||
|
|
||||||
if (process.env.REMOVE_TRAILING_SLASH) {
|
if (process.env.REMOVE_TRAILING_SLASH) {
|
||||||
url = url.replace(/\/$/, '');
|
url = url.replace(/\/$/, '');
|
||||||
}
|
}
|
||||||
|
|
||||||
const event_uuid = uuid();
|
const eventUuid = uuid();
|
||||||
|
|
||||||
if (type === 'pageview') {
|
if (type === 'pageview') {
|
||||||
await savePageView(website_id, { session_id, session_uuid, url, referrer });
|
await savePageView(website, { session, url, referrer });
|
||||||
} else if (type === 'event') {
|
} else if (type === 'event') {
|
||||||
await saveEvent(website_id, {
|
await saveEvent(website, {
|
||||||
event_uuid,
|
session,
|
||||||
session_id,
|
eventUuid,
|
||||||
session_uuid,
|
|
||||||
url,
|
url,
|
||||||
event_name,
|
eventName,
|
||||||
event_data,
|
eventData,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
return badRequest(res);
|
return badRequest(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = createToken({ website_id, session_id, session_uuid }, secret());
|
const token = createToken(
|
||||||
|
{
|
||||||
|
website,
|
||||||
|
session,
|
||||||
|
},
|
||||||
|
secret(),
|
||||||
|
);
|
||||||
|
|
||||||
return send(res, token);
|
return send(res, token);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,10 +8,10 @@ export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
const { user_id } = req.auth;
|
const { userId } = req.auth;
|
||||||
|
|
||||||
const websites = await getUserWebsites(user_id);
|
const websites = await getUserWebsites(userId);
|
||||||
const ids = websites.map(({ website_id }) => website_id);
|
const ids = websites.map(({ websiteUuid }) => websiteUuid);
|
||||||
const token = createToken({ websites: ids }, secret());
|
const token = createToken({ websites: ids }, secret());
|
||||||
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
|
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
|
||||||
|
|
||||||
|
|
|
@ -9,10 +9,10 @@ export default async (req, res) => {
|
||||||
const website = await getWebsiteByShareId(id);
|
const website = await getWebsiteByShareId(id);
|
||||||
|
|
||||||
if (website) {
|
if (website) {
|
||||||
const websiteId = website.website_id;
|
const { websiteId, websiteUuid } = website;
|
||||||
const token = createToken({ website_id: websiteId }, secret());
|
const token = createToken({ websiteId, websiteUuid }, secret());
|
||||||
|
|
||||||
return ok(res, { websiteId, token });
|
return ok(res, { websiteId, websiteUuid, token });
|
||||||
}
|
}
|
||||||
|
|
||||||
return notFound(res);
|
return notFound(res);
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
|
||||||
import { deleteWebsite, getWebsiteById } from 'queries';
|
|
||||||
import { allowQuery } from 'lib/auth';
|
|
||||||
import { useCors } from 'lib/middleware';
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
const { id } = req.query;
|
|
||||||
|
|
||||||
const websiteId = +id;
|
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
await useCors(req, res);
|
|
||||||
|
|
||||||
if (!(await allowQuery(req))) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const website = await getWebsiteById(websiteId);
|
|
||||||
|
|
||||||
return ok(res, website);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.method === 'DELETE') {
|
|
||||||
if (!(await allowQuery(req, true))) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
await deleteWebsite(websiteId);
|
|
||||||
|
|
||||||
return ok(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
|
||||||
};
|
|
|
@ -1,50 +0,0 @@
|
||||||
import moment from 'moment-timezone';
|
|
||||||
import { getPageviewStats } from 'queries';
|
|
||||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
|
||||||
import { allowQuery } from 'lib/auth';
|
|
||||||
import { useCors } from 'lib/middleware';
|
|
||||||
|
|
||||||
const unitTypes = ['year', 'month', 'hour', 'day'];
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
if (req.method === 'GET') {
|
|
||||||
await useCors(req, res);
|
|
||||||
|
|
||||||
if (!(await allowQuery(req))) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { id, start_at, end_at, unit, tz, url, referrer, os, browser, device, country } =
|
|
||||||
req.query;
|
|
||||||
|
|
||||||
const websiteId = +id;
|
|
||||||
const startDate = new Date(+start_at);
|
|
||||||
const endDate = new Date(+end_at);
|
|
||||||
|
|
||||||
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
|
||||||
return badRequest(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [pageviews, sessions] = await Promise.all([
|
|
||||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, '*', {
|
|
||||||
url,
|
|
||||||
referrer,
|
|
||||||
os,
|
|
||||||
browser,
|
|
||||||
device,
|
|
||||||
country,
|
|
||||||
}),
|
|
||||||
getPageviewStats(websiteId, startDate, endDate, tz, unit, 'distinct pageview.', {
|
|
||||||
url,
|
|
||||||
os,
|
|
||||||
browser,
|
|
||||||
device,
|
|
||||||
country,
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return ok(res, { pageviews, sessions });
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
|
||||||
};
|
|
|
@ -1,44 +0,0 @@
|
||||||
import { ok, unauthorized, methodNotAllowed, getRandomChars } from 'next-basics';
|
|
||||||
import { updateWebsite, createWebsite, getWebsiteById } from 'queries';
|
|
||||||
import { useAuth } from 'lib/middleware';
|
|
||||||
import { uuid } from 'lib/crypto';
|
|
||||||
|
|
||||||
export default async (req, res) => {
|
|
||||||
await useAuth(req, res);
|
|
||||||
|
|
||||||
const { user_id, is_admin } = req.auth;
|
|
||||||
const { website_id, enable_share_url } = req.body;
|
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
|
||||||
const { name, domain, owner } = req.body;
|
|
||||||
const website_owner = parseInt(owner);
|
|
||||||
|
|
||||||
if (website_id) {
|
|
||||||
const website = await getWebsiteById(website_id);
|
|
||||||
|
|
||||||
if (website.user_id !== user_id && !is_admin) {
|
|
||||||
return unauthorized(res);
|
|
||||||
}
|
|
||||||
|
|
||||||
let { share_id } = website;
|
|
||||||
|
|
||||||
if (enable_share_url) {
|
|
||||||
share_id = share_id ? share_id : getRandomChars(8);
|
|
||||||
} else {
|
|
||||||
share_id = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateWebsite(website_id, { name, domain, share_id, user_id: website_owner });
|
|
||||||
|
|
||||||
return ok(res);
|
|
||||||
} else {
|
|
||||||
const website_uuid = uuid();
|
|
||||||
const share_id = enable_share_url ? getRandomChars(8) : null;
|
|
||||||
const website = await createWebsite(website_owner, { website_uuid, name, domain, share_id });
|
|
||||||
|
|
||||||
return ok(res, website);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
|
||||||
};
|
|
|
@ -1,19 +1,18 @@
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
import { getActiveVisitors } from 'queries';
|
import { getActiveVisitors } from 'queries';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (req.method === 'GET') {
|
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
if (!(await allowQuery(req))) {
|
if (!(await allowQuery(req))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = req.query;
|
const { id: websiteId } = req.query;
|
||||||
|
|
||||||
const websiteId = +id;
|
|
||||||
|
|
||||||
const result = await getActiveVisitors(websiteId);
|
const result = await getActiveVisitors(websiteId);
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { getEventData } from 'queries';
|
||||||
|
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
|
import { allowQuery } from 'lib/auth';
|
||||||
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
if (!(await allowQuery(req))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { id: websiteId } = req.query;
|
||||||
|
|
||||||
|
const { start_at, end_at, timezone, event_name: eventName, columns, filters } = req.body;
|
||||||
|
|
||||||
|
if (!moment.tz.zone(timezone)) {
|
||||||
|
return badRequest(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const startDate = new Date(+start_at);
|
||||||
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
|
const events = await getEventData(websiteId, {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
timezone,
|
||||||
|
eventName,
|
||||||
|
columns,
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
return ok(res, events);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
|
@ -2,31 +2,30 @@ import moment from 'moment-timezone';
|
||||||
import { getEventMetrics } from 'queries';
|
import { getEventMetrics } from 'queries';
|
||||||
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
|
||||||
const unitTypes = ['year', 'month', 'hour', 'day'];
|
const unitTypes = ['year', 'month', 'hour', 'day'];
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (req.method === 'GET') {
|
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
if (!(await allowQuery(req))) {
|
if (!(await allowQuery(req))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, start_at, end_at, unit, tz, url, event_name } = req.query;
|
const { id: websiteId, start_at, end_at, unit, tz, url, event_name } = req.query;
|
||||||
|
|
||||||
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
||||||
return badRequest(res);
|
return badRequest(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const websiteId = +id;
|
|
||||||
const startDate = new Date(+start_at);
|
const startDate = new Date(+start_at);
|
||||||
const endDate = new Date(+end_at);
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, {
|
const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, {
|
||||||
url,
|
url,
|
||||||
event_name,
|
eventName: event_name,
|
||||||
});
|
});
|
||||||
|
|
||||||
return ok(res, events);
|
return ok(res, events);
|
|
@ -0,0 +1,69 @@
|
||||||
|
import { allowQuery } from 'lib/auth';
|
||||||
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
import { getRandomChars, methodNotAllowed, ok, serverError, unauthorized } from 'next-basics';
|
||||||
|
import { deleteWebsite, getAccount, getWebsite, updateWebsite } from 'queries';
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
const { id: websiteId } = req.query;
|
||||||
|
|
||||||
|
if (!(await allowQuery(req))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
const website = await getWebsite({ websiteUuid: websiteId });
|
||||||
|
|
||||||
|
return ok(res, website);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const { name, domain, owner, enableShareUrl, shareId } = req.body;
|
||||||
|
const { accountUuid } = req.auth;
|
||||||
|
let account;
|
||||||
|
|
||||||
|
if (accountUuid) {
|
||||||
|
account = await getAccount({ accountUuid });
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return serverError(res, 'Account does not exist.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const website = await getWebsite({ websiteUuid: websiteId });
|
||||||
|
|
||||||
|
const newShareId = enableShareUrl ? website.shareId || getRandomChars(8) : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await updateWebsite(
|
||||||
|
{
|
||||||
|
name,
|
||||||
|
domain,
|
||||||
|
shareId: shareId ? shareId : newShareId,
|
||||||
|
userId: account ? account.id : +owner || undefined,
|
||||||
|
},
|
||||||
|
{ websiteUuid: websiteId },
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
|
||||||
|
return serverError(res, 'That share ID is already taken.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.method === 'DELETE') {
|
||||||
|
if (!(await allowQuery(req, true))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteWebsite(websiteId);
|
||||||
|
|
||||||
|
return ok(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
|
@ -1,8 +1,8 @@
|
||||||
import { getPageviewMetrics, getSessionMetrics, getWebsiteById } from 'queries';
|
|
||||||
import { ok, methodNotAllowed, unauthorized, badRequest } from 'next-basics';
|
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
|
||||||
import { FILTER_IGNORED } from 'lib/constants';
|
import { FILTER_IGNORED } from 'lib/constants';
|
||||||
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
import { badRequest, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
|
import { getPageviewMetrics, getSessionMetrics, getWebsiteByUuid } from 'queries';
|
||||||
|
|
||||||
const sessionColumns = ['browser', 'os', 'device', 'screen', 'country', 'language'];
|
const sessionColumns = ['browser', 'os', 'device', 'screen', 'country', 'language'];
|
||||||
const pageviewColumns = ['url', 'referrer', 'query'];
|
const pageviewColumns = ['url', 'referrer', 'query'];
|
||||||
|
@ -34,25 +34,41 @@ function getColumn(type) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (req.method === 'GET') {
|
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
if (!(await allowQuery(req))) {
|
if (!(await allowQuery(req))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, type, start_at, end_at, url, referrer, os, browser, device, country } = req.query;
|
const {
|
||||||
|
id: websiteId,
|
||||||
const websiteId = +id;
|
type,
|
||||||
const startDate = new Date(+start_at);
|
start_at,
|
||||||
const endDate = new Date(+end_at);
|
end_at,
|
||||||
|
url,
|
||||||
if (sessionColumns.includes(type)) {
|
referrer,
|
||||||
let data = await getSessionMetrics(websiteId, startDate, endDate, type, {
|
|
||||||
os,
|
os,
|
||||||
browser,
|
browser,
|
||||||
device,
|
device,
|
||||||
country,
|
country,
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
const startDate = new Date(+start_at);
|
||||||
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
|
if (sessionColumns.includes(type)) {
|
||||||
|
let data = await getSessionMetrics(websiteId, {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
field: type,
|
||||||
|
filters: {
|
||||||
|
os,
|
||||||
|
browser,
|
||||||
|
device,
|
||||||
|
country,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === 'language') {
|
if (type === 'language') {
|
||||||
|
@ -78,7 +94,7 @@ export default async (req, res) => {
|
||||||
let domain;
|
let domain;
|
||||||
|
|
||||||
if (type === 'referrer') {
|
if (type === 'referrer') {
|
||||||
const website = await getWebsiteById(websiteId);
|
const website = await getWebsiteByUuid(websiteId);
|
||||||
|
|
||||||
if (!website) {
|
if (!website) {
|
||||||
return badRequest(res);
|
return badRequest(res);
|
||||||
|
@ -101,7 +117,13 @@ export default async (req, res) => {
|
||||||
query: type === 'query' && table !== 'event' ? true : undefined,
|
query: type === 'query' && table !== 'event' ? true : undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const data = await getPageviewMetrics(websiteId, startDate, endDate, column, table, filters);
|
const data = await getPageviewMetrics(websiteId, {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
column,
|
||||||
|
table,
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
|
||||||
return ok(res, data);
|
return ok(res, data);
|
||||||
}
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { getPageviewStats } from 'queries';
|
||||||
|
import { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
||||||
|
import { allowQuery } from 'lib/auth';
|
||||||
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
|
||||||
|
const unitTypes = ['year', 'month', 'hour', 'day'];
|
||||||
|
|
||||||
|
export default async (req, res) => {
|
||||||
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
if (!(await allowQuery(req))) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
id: websiteId,
|
||||||
|
start_at,
|
||||||
|
end_at,
|
||||||
|
unit,
|
||||||
|
tz,
|
||||||
|
url,
|
||||||
|
referrer,
|
||||||
|
os,
|
||||||
|
browser,
|
||||||
|
device,
|
||||||
|
country,
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
|
const startDate = new Date(+start_at);
|
||||||
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
|
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
|
||||||
|
return badRequest(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [pageviews, sessions] = await Promise.all([
|
||||||
|
getPageviewStats(websiteId, {
|
||||||
|
start_at: startDate,
|
||||||
|
end_at: endDate,
|
||||||
|
timezone: tz,
|
||||||
|
unit,
|
||||||
|
count: '*',
|
||||||
|
filters: {
|
||||||
|
url,
|
||||||
|
referrer,
|
||||||
|
os,
|
||||||
|
browser,
|
||||||
|
device,
|
||||||
|
country,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
getPageviewStats(websiteId, {
|
||||||
|
start_at: startDate,
|
||||||
|
end_at: endDate,
|
||||||
|
timezone: tz,
|
||||||
|
unit,
|
||||||
|
count: 'distinct pageview.',
|
||||||
|
filters: {
|
||||||
|
url,
|
||||||
|
os,
|
||||||
|
browser,
|
||||||
|
device,
|
||||||
|
country,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return ok(res, { pageviews, sessions });
|
||||||
|
}
|
||||||
|
|
||||||
|
return methodNotAllowed(res);
|
||||||
|
};
|
|
@ -1,10 +1,13 @@
|
||||||
import { resetWebsite } from 'queries';
|
import { resetWebsite } from 'queries';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
const { id } = req.query;
|
await useCors(req, res);
|
||||||
const websiteId = +id;
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
const { id: websiteId } = req.query;
|
||||||
|
|
||||||
if (req.method === 'POST') {
|
if (req.method === 'POST') {
|
||||||
if (!(await allowQuery(req))) {
|
if (!(await allowQuery(req))) {
|
|
@ -1,19 +1,29 @@
|
||||||
import { getWebsiteStats } from 'queries';
|
import { getWebsiteStats } from 'queries';
|
||||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||||
import { allowQuery } from 'lib/auth';
|
import { allowQuery } from 'lib/auth';
|
||||||
import { useCors } from 'lib/middleware';
|
import { useAuth, useCors } from 'lib/middleware';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
if (req.method === 'GET') {
|
|
||||||
await useCors(req, res);
|
await useCors(req, res);
|
||||||
|
await useAuth(req, res);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
if (!(await allowQuery(req))) {
|
if (!(await allowQuery(req))) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id, start_at, end_at, url, referrer, os, browser, device, country } = req.query;
|
const {
|
||||||
|
id: websiteId,
|
||||||
|
start_at,
|
||||||
|
end_at,
|
||||||
|
url,
|
||||||
|
referrer,
|
||||||
|
os,
|
||||||
|
browser,
|
||||||
|
device,
|
||||||
|
country,
|
||||||
|
} = req.query;
|
||||||
|
|
||||||
const websiteId = +id;
|
|
||||||
const startDate = new Date(+start_at);
|
const startDate = new Date(+start_at);
|
||||||
const endDate = new Date(+end_at);
|
const endDate = new Date(+end_at);
|
||||||
|
|
||||||
|
@ -21,21 +31,29 @@ export default async (req, res) => {
|
||||||
const prevStartDate = new Date(+start_at - distance);
|
const prevStartDate = new Date(+start_at - distance);
|
||||||
const prevEndDate = new Date(+end_at - distance);
|
const prevEndDate = new Date(+end_at - distance);
|
||||||
|
|
||||||
const metrics = await getWebsiteStats(websiteId, startDate, endDate, {
|
const metrics = await getWebsiteStats(websiteId, {
|
||||||
|
start_at: startDate,
|
||||||
|
end_at: endDate,
|
||||||
|
filters: {
|
||||||
url,
|
url,
|
||||||
referrer,
|
referrer,
|
||||||
os,
|
os,
|
||||||
browser,
|
browser,
|
||||||
device,
|
device,
|
||||||
country,
|
country,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, {
|
const prevPeriod = await getWebsiteStats(websiteId, {
|
||||||
|
start_at: prevStartDate,
|
||||||
|
end_at: prevEndDate,
|
||||||
|
filters: {
|
||||||
url,
|
url,
|
||||||
referrer,
|
referrer,
|
||||||
os,
|
os,
|
||||||
browser,
|
browser,
|
||||||
device,
|
device,
|
||||||
country,
|
country,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
const stats = Object.keys(metrics[0]).reduce((obj, key) => {
|
|
@ -1,26 +1,49 @@
|
||||||
import { getAllWebsites, getUserWebsites } from 'queries';
|
import { createWebsite, getAccount, getAllWebsites, getUserWebsites } from 'queries';
|
||||||
|
import { ok, methodNotAllowed, unauthorized, getRandomChars } from 'next-basics';
|
||||||
import { useAuth } from 'lib/middleware';
|
import { useAuth } from 'lib/middleware';
|
||||||
import { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
import { uuid } from 'lib/crypto';
|
||||||
|
|
||||||
export default async (req, res) => {
|
export default async (req, res) => {
|
||||||
await useAuth(req, res);
|
await useAuth(req, res);
|
||||||
|
|
||||||
const { user_id: current_user_id, is_admin } = req.auth;
|
const { userId: currentUserId, isAdmin, accountUuid } = req.auth;
|
||||||
const { user_id, include_all } = req.query;
|
const { user_id, include_all } = req.query;
|
||||||
const userId = +user_id;
|
let account;
|
||||||
|
|
||||||
|
if (accountUuid) {
|
||||||
|
account = await getAccount({ accountUuid: accountUuid });
|
||||||
|
}
|
||||||
|
|
||||||
|
const userId = account ? account.id : +user_id;
|
||||||
|
|
||||||
if (req.method === 'GET') {
|
if (req.method === 'GET') {
|
||||||
if (userId && userId !== current_user_id && !is_admin) {
|
if (userId && userId !== currentUserId && !isAdmin) {
|
||||||
return unauthorized(res);
|
return unauthorized(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
const websites =
|
const websites =
|
||||||
is_admin && include_all
|
isAdmin && include_all
|
||||||
? await getAllWebsites()
|
? await getAllWebsites()
|
||||||
: await getUserWebsites(userId || current_user_id);
|
: await getUserWebsites(userId || currentUserId);
|
||||||
|
|
||||||
return ok(res, websites);
|
return ok(res, websites);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (req.method === 'POST') {
|
||||||
|
const { name, domain, owner, enableShareUrl } = req.body;
|
||||||
|
|
||||||
|
const website_owner = account ? account.id : +owner;
|
||||||
|
|
||||||
|
if (website_owner !== currentUserId && !isAdmin) {
|
||||||
|
return unauthorized(res);
|
||||||
|
}
|
||||||
|
|
||||||
|
const websiteUuid = uuid();
|
||||||
|
const shareId = enableShareUrl ? getRandomChars(8) : null;
|
||||||
|
const website = await createWebsite(website_owner, { websiteUuid, name, domain, shareId });
|
||||||
|
|
||||||
|
return ok(res, website);
|
||||||
|
}
|
||||||
|
|
||||||
return methodNotAllowed(res);
|
return methodNotAllowed(res);
|
||||||
};
|
};
|
||||||
|
|
|
@ -8,7 +8,7 @@ export default function ConsolePage({ enabled }) {
|
||||||
const { loading } = useRequireLogin();
|
const { loading } = useRequireLogin();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
if (loading || !enabled || !user?.is_admin) {
|
if (loading || !enabled || !user?.isAdmin) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,6 +16,6 @@ export default function LoginPage({ loginDisabled }) {
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
return {
|
return {
|
||||||
props: { loginDisabled: !!process.env.DISABLE_LOGIN },
|
props: { loginDisabled: !!process.env.DISABLE_LOGIN || !!process.env.isCloudMode },
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import useRequireLogin from 'hooks/useRequireLogin';
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { loading } = useRequireLogin();
|
const { loading } = useRequireLogin();
|
||||||
|
|
||||||
if (loading) {
|
if (process.env.isCloudMode || loading) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,11 +14,11 @@ export default function SharePage() {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { websiteId } = shareToken;
|
const { websiteUuid } = shareToken;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<WebsiteDetails websiteId={websiteId} />
|
<WebsiteDetails websiteId={websiteUuid} />
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,18 @@
|
||||||
"value": "Add account"
|
"value": "Add account"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.add-column": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Add column"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.add-filter": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Add filter"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.add-website": [
|
"label.add-website": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -155,6 +167,18 @@
|
||||||
"value": "Enable share URL"
|
"value": "Enable share URL"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.event-data": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Event Data"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"label.field-name": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Field Name"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.invalid": [
|
"label.invalid": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -313,6 +337,12 @@
|
||||||
"value": "Save"
|
"value": "Save"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.search": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Search"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.settings": [
|
"label.settings": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -373,6 +403,12 @@
|
||||||
"value": "Tracking code"
|
"value": "Tracking code"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.type": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Type"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.unknown": [
|
"label.unknown": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -385,6 +421,12 @@
|
||||||
"value": "Username"
|
"value": "Username"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.value": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Value"
|
||||||
|
}
|
||||||
|
],
|
||||||
"label.view-details": [
|
"label.view-details": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
|
@ -244,7 +244,7 @@
|
||||||
"label.none": [
|
"label.none": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "None"
|
"value": "Байхгүй"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"label.owner": [
|
"label.owner": [
|
||||||
|
@ -397,6 +397,12 @@
|
||||||
"value": "Вебүүд"
|
"value": "Вебүүд"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "Өчигдөр"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
@ -456,7 +462,7 @@
|
||||||
"message.confirm-reset": [
|
"message.confirm-reset": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Are your sure you want to reset "
|
"value": "Та "
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 1,
|
"type": 1,
|
||||||
|
@ -464,7 +470,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "'s statistics?"
|
"value": "-н тоон үзүүлэлтүүдийг устгахдаа итгэлтэй байна уу?"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.copied": [
|
"message.copied": [
|
||||||
|
@ -482,7 +488,7 @@
|
||||||
"message.edit-dashboard": [
|
"message.edit-dashboard": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Edit dashboard"
|
"value": "Хянах самбар засах"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"message.failure": [
|
"message.failure": [
|
||||||
|
@ -770,7 +776,7 @@
|
||||||
"metrics.query-parameters": [
|
"metrics.query-parameters": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Query parameters"
|
"value": "Query параметр"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.referrers": [
|
"metrics.referrers": [
|
||||||
|
@ -782,7 +788,7 @@
|
||||||
"metrics.screens": [
|
"metrics.screens": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
"value": "Screens"
|
"value": "Дэлгэц"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"metrics.unique-visitors": [
|
"metrics.unique-visitors": [
|
||||||
|
|
|
@ -397,6 +397,12 @@
|
||||||
"value": "网站"
|
"value": "网站"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"label.yesterday": [
|
||||||
|
{
|
||||||
|
"type": 0,
|
||||||
|
"value": "昨天"
|
||||||
|
}
|
||||||
|
],
|
||||||
"message.active-users": [
|
"message.active-users": [
|
||||||
{
|
{
|
||||||
"type": 0,
|
"type": 0,
|
||||||
|
|
|
@ -1,47 +1,47 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import redis, { DELETED } from 'lib/redis';
|
import redis, { DELETED } from 'lib/redis';
|
||||||
|
|
||||||
export async function deleteAccount(user_id) {
|
export async function deleteAccount(userId) {
|
||||||
const { client } = prisma;
|
const { client } = prisma;
|
||||||
|
|
||||||
const websites = await client.website.findMany({
|
const websites = await client.website.findMany({
|
||||||
where: { user_id },
|
where: { userId },
|
||||||
select: { website_uuid: true },
|
select: { websiteUuid: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
let websiteUuids = [];
|
let websiteUuids = [];
|
||||||
|
|
||||||
if (websites.length > 0) {
|
if (websites.length > 0) {
|
||||||
websiteUuids = websites.map(a => a.website_uuid);
|
websiteUuids = websites.map(a => a.websiteUuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return client
|
return client
|
||||||
.$transaction([
|
.$transaction([
|
||||||
client.pageview.deleteMany({
|
client.pageview.deleteMany({
|
||||||
where: { session: { website: { user_id } } },
|
where: { session: { website: { userId } } },
|
||||||
}),
|
}),
|
||||||
client.event_data.deleteMany({
|
client.eventData.deleteMany({
|
||||||
where: { event: { session: { website: { user_id } } } },
|
where: { event: { session: { website: { userId } } } },
|
||||||
}),
|
}),
|
||||||
client.event.deleteMany({
|
client.event.deleteMany({
|
||||||
where: { session: { website: { user_id } } },
|
where: { session: { website: { userId } } },
|
||||||
}),
|
}),
|
||||||
client.session.deleteMany({
|
client.session.deleteMany({
|
||||||
where: { website: { user_id } },
|
where: { website: { userId } },
|
||||||
}),
|
}),
|
||||||
client.website.deleteMany({
|
client.website.deleteMany({
|
||||||
where: { user_id },
|
where: { userId },
|
||||||
}),
|
}),
|
||||||
client.account.delete({
|
client.account.delete({
|
||||||
where: {
|
where: {
|
||||||
user_id,
|
id: userId,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
if (redis.client) {
|
if (redis.client) {
|
||||||
for (let i = 0; i < websiteUuids.length; i++) {
|
for (let i = 0; i < websiteUuids.length; i++) {
|
||||||
await redis.client.set(`website:${websiteUuids[i]}`, DELETED);
|
await redis.set(`website:${websiteUuids[i]}`, DELETED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
|
export async function getAccount(where) {
|
||||||
|
return prisma.client.account.findUnique({
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function getAccountById(user_id) {
|
export async function getAccountById(userId) {
|
||||||
return prisma.client.account.findUnique({
|
return prisma.client.account.findUnique({
|
||||||
where: {
|
where: {
|
||||||
user_id,
|
id: userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,17 +3,17 @@ import prisma from 'lib/prisma';
|
||||||
export async function getAccounts() {
|
export async function getAccounts() {
|
||||||
return prisma.client.account.findMany({
|
return prisma.client.account.findMany({
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{ is_admin: 'desc' },
|
{ isAdmin: 'desc' },
|
||||||
{
|
{
|
||||||
username: 'asc',
|
username: 'asc',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
select: {
|
select: {
|
||||||
user_id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
is_admin: true,
|
isAdmin: true,
|
||||||
created_at: true,
|
createdAt: true,
|
||||||
updated_at: true,
|
updatedAt: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function updateAccount(user_id, data) {
|
export async function updateAccount(data, where) {
|
||||||
return prisma.client.account.update({
|
return prisma.client.account.update({
|
||||||
where: {
|
where,
|
||||||
user_id,
|
|
||||||
},
|
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import redis from 'lib/redis';
|
import redis from 'lib/redis';
|
||||||
|
|
||||||
export async function createWebsite(user_id, data) {
|
export async function createWebsite(userId, data) {
|
||||||
return prisma.client.website
|
return prisma.client.website
|
||||||
.create({
|
.create({
|
||||||
data: {
|
data: {
|
||||||
account: {
|
account: {
|
||||||
connect: {
|
connect: {
|
||||||
user_id,
|
id: userId,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
...data,
|
...data,
|
||||||
|
@ -15,7 +15,7 @@ export async function createWebsite(user_id, data) {
|
||||||
})
|
})
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
if (redis.client && res) {
|
if (redis.client && res) {
|
||||||
await redis.client.set(`website:${res.website_uuid}`, res.website_id);
|
await redis.client.set(`website:${res.websiteUuid}`, res.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import redis, { DELETED } from 'lib/redis';
|
import redis, { DELETED } from 'lib/redis';
|
||||||
import { getWebsiteById } from 'queries';
|
import { getWebsiteByUuid } from 'queries';
|
||||||
|
|
||||||
export async function deleteWebsite(website_id) {
|
export async function deleteWebsite(websiteId) {
|
||||||
const { client, transaction } = prisma;
|
const { client, transaction } = prisma;
|
||||||
|
|
||||||
const { website_uuid } = await getWebsiteById(website_id);
|
const { websiteUuid } = await getWebsiteByUuid(websiteId);
|
||||||
|
|
||||||
return transaction([
|
return transaction([
|
||||||
client.pageview.deleteMany({
|
client.pageview.deleteMany({
|
||||||
where: { session: { website: { website_id } } },
|
where: { session: { website: { websiteUuid: websiteId } } },
|
||||||
}),
|
}),
|
||||||
client.event_data.deleteMany({
|
client.eventData.deleteMany({
|
||||||
where: { event: { session: { website: { website_id } } } },
|
where: { event: { session: { website: { websiteUuid: websiteId } } } },
|
||||||
}),
|
}),
|
||||||
client.event.deleteMany({
|
client.event.deleteMany({
|
||||||
where: { session: { website: { website_id } } },
|
where: { session: { website: { websiteUuid: websiteId } } },
|
||||||
}),
|
}),
|
||||||
client.session.deleteMany({
|
client.session.deleteMany({
|
||||||
where: { website: { website_id } },
|
where: { website: { websiteUuid: websiteId } },
|
||||||
}),
|
}),
|
||||||
client.website.delete({
|
client.website.delete({
|
||||||
where: { website_id },
|
where: { websiteUuid: websiteId },
|
||||||
}),
|
}),
|
||||||
]).then(async res => {
|
]).then(async res => {
|
||||||
if (redis.client) {
|
if (redis.client) {
|
||||||
await redis.client.set(`website:${website_uuid}`, DELETED);
|
await redis.client.set(`website:${websiteUuid}`, DELETED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -4,7 +4,7 @@ export async function getAllWebsites() {
|
||||||
let data = await prisma.client.website.findMany({
|
let data = await prisma.client.website.findMany({
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
user_id: 'asc',
|
userId: 'asc',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'asc',
|
name: 'asc',
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function getUserWebsites(user_id) {
|
export async function getUserWebsites(userId) {
|
||||||
return prisma.client.website.findMany({
|
return prisma.client.website.findMany({
|
||||||
where: {
|
where: {
|
||||||
user_id,
|
userId,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
name: 'asc',
|
name: 'asc',
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
|
export async function getWebsite(where) {
|
||||||
|
return prisma.client.website.findUnique({
|
||||||
|
where,
|
||||||
|
});
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function getWebsiteById(website_id) {
|
export async function getWebsiteById(websiteId) {
|
||||||
return prisma.client.website.findUnique({
|
return prisma.client.website.findUnique({
|
||||||
where: {
|
where: {
|
||||||
website_id,
|
id: websiteId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function getWebsiteByShareId(share_id) {
|
export async function getWebsiteByShareId(shareId) {
|
||||||
return prisma.client.website.findUnique({
|
return prisma.client.website.findUnique({
|
||||||
where: {
|
where: {
|
||||||
share_id,
|
shareId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
import redis from 'lib/redis';
|
import redis from 'lib/redis';
|
||||||
|
|
||||||
export async function getWebsiteByUuid(website_uuid) {
|
export async function getWebsiteByUuid(websiteUuid) {
|
||||||
return prisma.client.website
|
return prisma.client.website
|
||||||
.findUnique({
|
.findUnique({
|
||||||
where: {
|
where: {
|
||||||
website_uuid,
|
websiteUuid,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(async res => {
|
.then(async res => {
|
||||||
if (redis.client && res) {
|
if (redis.client && res) {
|
||||||
await redis.client.set(`website:${res.website_uuid}`, 1);
|
await redis.client.set(`website:${res.websiteUuid}`, res.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res;
|
return res;
|
||||||
|
|
|
@ -1,20 +1,20 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function resetWebsite(website_id) {
|
export async function resetWebsite(websiteId) {
|
||||||
const { client, transaction } = prisma;
|
const { client, transaction } = prisma;
|
||||||
|
|
||||||
return transaction([
|
return transaction([
|
||||||
client.pageview.deleteMany({
|
client.pageview.deleteMany({
|
||||||
where: { session: { website: { website_id } } },
|
where: { session: { website: { websiteUuid: websiteId } } },
|
||||||
}),
|
}),
|
||||||
client.event_data.deleteMany({
|
client.eventData.deleteMany({
|
||||||
where: { event: { session: { website: { website_id } } } },
|
where: { event: { session: { website: { websiteUuid: websiteId } } } },
|
||||||
}),
|
}),
|
||||||
client.event.deleteMany({
|
client.event.deleteMany({
|
||||||
where: { session: { website: { website_id } } },
|
where: { session: { website: { websiteUuid: websiteId } } },
|
||||||
}),
|
}),
|
||||||
client.session.deleteMany({
|
client.session.deleteMany({
|
||||||
where: { website: { website_id } },
|
where: { website: { websiteUuid: websiteId } },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import prisma from 'lib/prisma';
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
export async function updateWebsite(website_id, data) {
|
export async function updateWebsite(data, where) {
|
||||||
return prisma.client.website.update({
|
return prisma.client.website.update({
|
||||||
where: {
|
where,
|
||||||
website_id,
|
|
||||||
},
|
|
||||||
data,
|
data,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,63 @@
|
||||||
|
import clickhouse from 'lib/clickhouse';
|
||||||
|
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||||
|
import prisma from 'lib/prisma';
|
||||||
|
|
||||||
|
export async function getEventData(...args) {
|
||||||
|
return runQuery({
|
||||||
|
[PRISMA]: () => relationalQuery(...args),
|
||||||
|
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function relationalQuery(websiteId, { startDate, endDate, event_name, columns, filters }) {
|
||||||
|
const { rawQuery, getEventDataColumnsQuery, getEventDataFilterQuery } = prisma;
|
||||||
|
const params = [startDate, endDate];
|
||||||
|
|
||||||
|
return rawQuery(
|
||||||
|
`select
|
||||||
|
${getEventDataColumnsQuery('event_data.event_data', columns)}
|
||||||
|
from event
|
||||||
|
join website
|
||||||
|
on event.website_id = website.website_id
|
||||||
|
join event_data
|
||||||
|
on event.event_id = event_data.event_id
|
||||||
|
where website_uuid='${websiteId}'
|
||||||
|
and event.created_at between $1 and $2
|
||||||
|
${event_name ? `and event_name = ${event_name}` : ''}
|
||||||
|
${
|
||||||
|
Object.keys(filters).length > 0
|
||||||
|
? `and ${getEventDataFilterQuery('event_data.event_data', filters)}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
params,
|
||||||
|
).then(results => {
|
||||||
|
return Object.keys(results[0]).map(a => {
|
||||||
|
return { x: a, y: results[0][`${a}`] };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function clickhouseQuery(websiteId, { startDate, endDate, event_name, columns, filters }) {
|
||||||
|
const { rawQuery, getBetweenDates, getEventDataColumnsQuery, getEventDataFilterQuery } =
|
||||||
|
clickhouse;
|
||||||
|
const params = [websiteId];
|
||||||
|
|
||||||
|
return rawQuery(
|
||||||
|
`select
|
||||||
|
${getEventDataColumnsQuery('event_data', columns)}
|
||||||
|
from event
|
||||||
|
where website_id= $1
|
||||||
|
${event_name ? `and event_name = ${event_name}` : ''}
|
||||||
|
and ${getBetweenDates('created_at', startDate, endDate)}
|
||||||
|
${
|
||||||
|
Object.keys(filters).length > 0
|
||||||
|
? `and ${getEventDataFilterQuery('event_data', filters)}`
|
||||||
|
: ''
|
||||||
|
}`,
|
||||||
|
params,
|
||||||
|
).then(results => {
|
||||||
|
return Object.keys(results[0]).map(a => {
|
||||||
|
return { x: a, y: results[0][`${a}`] };
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue