mirror of
https://github.com/kremalicious/umami.git
synced 2025-01-11 13:44:01 +01:00
commit
80f22313f6
@ -48,13 +48,7 @@ mysql://username:mypassword@localhost:3306/mydb
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Create database tables
|
||||
|
||||
```bash
|
||||
yarn update-db
|
||||
```
|
||||
|
||||
This will also create a login account with username **admin** and password **umami**.
|
||||
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**.
|
||||
|
||||
### Start the application
|
||||
|
||||
|
48
components/common/EventDataButton.js
Normal file
48
components/common/EventDataButton.js
Normal file
@ -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;
|
3
components/common/EventDataButton.module.css
Normal file
3
components/common/EventDataButton.module.css
Normal file
@ -0,0 +1,3 @@
|
||||
.button {
|
||||
width: fit-content;
|
||||
}
|
@ -11,7 +11,7 @@ import useDateRange from 'hooks/useDateRange';
|
||||
function RefreshButton({ websiteId }) {
|
||||
const [dateRange] = useDateRange(websiteId);
|
||||
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);
|
||||
|
||||
function handleClick() {
|
||||
|
@ -18,7 +18,7 @@ export default function UpdateNotice() {
|
||||
function handleViewClick() {
|
||||
updateCheck();
|
||||
setDismissed(true);
|
||||
location.href = releaseUrl || REPO_URL;
|
||||
open(releaseUrl || REPO_URL, '_blank');
|
||||
}
|
||||
|
||||
function handleDismissClick() {
|
||||
|
@ -15,13 +15,13 @@ const initialValues = {
|
||||
password: '',
|
||||
};
|
||||
|
||||
const validate = ({ user_id, username, password }) => {
|
||||
const validate = ({ id, username, password }) => {
|
||||
const errors = {};
|
||||
|
||||
if (!username) {
|
||||
errors.username = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
if (!user_id && !password) {
|
||||
if (!id && !password) {
|
||||
errors.password = <FormattedMessage id="label.required" defaultMessage="Required" />;
|
||||
}
|
||||
|
||||
@ -33,7 +33,8 @@ export default function AccountEditForm({ values, onSave, onClose }) {
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
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) {
|
||||
onSave();
|
||||
|
@ -9,6 +9,7 @@ import FormLayout, {
|
||||
FormRow,
|
||||
} from 'components/layout/FormLayout';
|
||||
import useApi from 'hooks/useApi';
|
||||
import useUser from '../../hooks/useUser';
|
||||
|
||||
const initialValues = {
|
||||
current_password: '',
|
||||
@ -39,9 +40,10 @@ const validate = ({ current_password, new_password, confirm_password }) => {
|
||||
export default function ChangePasswordForm({ values, onSave, onClose }) {
|
||||
const { post } = useApi();
|
||||
const [message, setMessage] = useState();
|
||||
const { user } = useUser();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { ok, data } = await post('/account/password', values);
|
||||
const { ok, data } = await post(`/accounts/${user.userId}/password`, values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
|
262
components/forms/EventDataForm.js
Normal file
262
components/forms/EventDataForm.js
Normal file
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
38
components/forms/EventDataForm.module.css
Normal file
38
components/forms/EventDataForm.module.css
Normal file
@ -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 }) {
|
||||
const ref = useRef();
|
||||
const { basePath } = useRouter();
|
||||
const { name, share_id } = values;
|
||||
const { name, shareId } = values;
|
||||
|
||||
return (
|
||||
<FormLayout>
|
||||
@ -27,7 +27,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
||||
spellCheck={false}
|
||||
defaultValue={`${
|
||||
document.location.origin
|
||||
}${basePath}/share/${share_id}/${encodeURIComponent(name)}`}
|
||||
}${basePath}/share/${shareId}/${encodeURIComponent(name)}`}
|
||||
readOnly
|
||||
/>
|
||||
</FormRow>
|
||||
|
@ -26,7 +26,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
||||
rows={3}
|
||||
cols={60}
|
||||
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
|
||||
}${basePath}/${trackerScriptName ? `${trackerScriptName}.js` : 'umami.js'}"></script>`}
|
||||
readOnly
|
||||
|
@ -38,18 +38,17 @@ const validate = ({ name, domain }) => {
|
||||
};
|
||||
|
||||
const OwnerDropDown = ({ user, accounts }) => {
|
||||
console.info(styles);
|
||||
const { setFieldValue, values } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
if (values.user_id != null && values.owner === '') {
|
||||
setFieldValue('owner', values.user_id.toString());
|
||||
} else if (user?.user_id && values.owner === '') {
|
||||
setFieldValue('owner', user.user_id.toString());
|
||||
if (values.userId != null && values.owner === '') {
|
||||
setFieldValue('owner', values.userId.toString());
|
||||
} else if (user?.id && values.owner === '') {
|
||||
setFieldValue('owner', user.id.toString());
|
||||
}
|
||||
}, [accounts, setFieldValue, user, values]);
|
||||
|
||||
if (user?.is_admin) {
|
||||
if (user?.isAdmin) {
|
||||
return (
|
||||
<FormRow>
|
||||
<label htmlFor="owner">
|
||||
@ -58,7 +57,7 @@ const OwnerDropDown = ({ user, accounts }) => {
|
||||
<div>
|
||||
<Field as="select" name="owner" className={styles.dropdown}>
|
||||
{accounts?.map(acc => (
|
||||
<option key={acc.user_id} value={acc.user_id}>
|
||||
<option key={acc.id} value={acc.id}>
|
||||
{acc.username}
|
||||
</option>
|
||||
))}
|
||||
@ -79,7 +78,9 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
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) {
|
||||
onSave();
|
||||
@ -93,7 +94,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
return (
|
||||
<FormLayout>
|
||||
<Formik
|
||||
initialValues={{ ...initialValues, ...values, enable_share_url: !!values?.share_id }}
|
||||
initialValues={{ ...initialValues, ...values, enableShareUrl: !!values?.shareId }}
|
||||
validate={validate}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
@ -117,9 +118,9 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
name="domain"
|
||||
type="text"
|
||||
placeholder="example.com"
|
||||
spellcheck="false"
|
||||
autocapitalize="off"
|
||||
autocorrect="off"
|
||||
spellCheck="false"
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
/>
|
||||
<FormError name="domain" />
|
||||
</div>
|
||||
@ -127,7 +128,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
<OwnerDropDown accounts={accounts} user={user} />
|
||||
<FormRow>
|
||||
<label />
|
||||
<Field name="enable_share_url">
|
||||
<Field name="enableShareUrl">
|
||||
{({ field }) => (
|
||||
<Checkbox
|
||||
{...field}
|
||||
|
@ -19,7 +19,7 @@ export default function Header() {
|
||||
const { pathname } = useRouter();
|
||||
const { updatesDisabled } = useConfig();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
const allowUpdate = user?.is_admin && !updatesDisabled && !isSharePage;
|
||||
const allowUpdate = user?.isAdmin && !updatesDisabled && !isSharePage;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -38,9 +38,11 @@ export default function Header() {
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
{!process.env.isCloudMode && (
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.buttons}>
|
||||
|
@ -6,7 +6,7 @@ import Dot from 'components/common/Dot';
|
||||
import styles from './ActiveUsers.module.css';
|
||||
|
||||
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, {
|
||||
interval,
|
||||
});
|
||||
|
@ -16,6 +16,7 @@ export default function DataTable({
|
||||
height,
|
||||
animate = true,
|
||||
virtualize = false,
|
||||
showPercentage = true,
|
||||
}) {
|
||||
const [format, setFormat] = useState(true);
|
||||
const formatFunc = format ? formatLongNumber : formatNumber;
|
||||
@ -38,6 +39,7 @@ export default function DataTable({
|
||||
animate={animate && !virtualize}
|
||||
format={formatFunc}
|
||||
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({
|
||||
width: percent,
|
||||
y: value,
|
||||
@ -82,15 +92,17 @@ const AnimatedRow = ({ label, value = 0, percent, animate, format, onClick }) =>
|
||||
<div className={styles.value} onClick={onClick}>
|
||||
<animated.div className={styles.value}>{props.y?.interpolate(format)}</animated.div>
|
||||
</div>
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
{showPercentage && (
|
||||
<div className={styles.percent}>
|
||||
<animated.div
|
||||
className={styles.bar}
|
||||
style={{ width: props.width.interpolate(n => `${n}%`) }}
|
||||
/>
|
||||
<animated.span className={styles.percentValue}>
|
||||
{props.width.interpolate(n => `${n.toFixed(0)}%`)}
|
||||
</animated.span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -16,7 +16,7 @@ export default function EventsChart({ websiteId, className, token }) {
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, loading } = useFetch(
|
||||
`/website/${websiteId}/events`,
|
||||
`/websites/${websiteId}/events`,
|
||||
{
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
|
@ -5,12 +5,12 @@ import Button from 'components/common/Button';
|
||||
import Times from 'assets/times.svg';
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className={classNames(styles.filters, 'col-12')}>
|
||||
<div className={classNames(styles.filters, 'col-12', className)}>
|
||||
{Object.keys(params).map(key => {
|
||||
if (!params[key]) {
|
||||
return null;
|
||||
|
@ -7,8 +7,5 @@
|
||||
.tag {
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.tag + .tag {
|
||||
margin-left: 20px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ export default function MetricsBar({ websiteId, className }) {
|
||||
} = usePageQuery();
|
||||
|
||||
const { data, error, loading } = useFetch(
|
||||
`/website/${websiteId}/stats`,
|
||||
`/websites/${websiteId}/stats`,
|
||||
{
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
|
@ -38,7 +38,7 @@ export default function MetricsTable({
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
const { data, loading, error } = useFetch(
|
||||
`/website/${websiteId}/metrics`,
|
||||
`/websites/${websiteId}/metrics`,
|
||||
{
|
||||
params: {
|
||||
type,
|
||||
|
@ -9,8 +9,8 @@ function mapData(data) {
|
||||
const arr = [];
|
||||
|
||||
data.reduce((obj, val) => {
|
||||
const { created_at } = val;
|
||||
const t = startOfMinute(parseISO(created_at));
|
||||
const { createdAt } = val;
|
||||
const t = startOfMinute(parseISO(createdAt));
|
||||
if (t.getTime() > last) {
|
||||
obj = { t: format(t, 'yyyy-LL-dd HH:mm:00'), y: 1 };
|
||||
arr.push(obj);
|
||||
|
@ -11,9 +11,9 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
|
||||
const options = [
|
||||
{ label: <FormattedMessage id="label.all-websites" defaultMessage="All websites" />, value: 0 },
|
||||
].concat(
|
||||
websites.map(({ name, website_id }, index) => ({
|
||||
websites.map(({ name, id }, index) => ({
|
||||
label: name,
|
||||
value: website_id,
|
||||
value: id,
|
||||
divider: index === 0,
|
||||
})),
|
||||
);
|
||||
@ -22,7 +22,7 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
|
||||
|
||||
const count = useMemo(() => {
|
||||
return sessions.filter(
|
||||
({ created_at }) => differenceInMinutes(new Date(), new Date(created_at)) <= 5,
|
||||
({ createdAt }) => differenceInMinutes(new Date(), new Date(createdAt)) <= 5,
|
||||
).length;
|
||||
}, [sessions, websiteId]);
|
||||
|
||||
|
@ -15,6 +15,7 @@ import Visitor from 'assets/visitor.svg';
|
||||
import Eye from 'assets/eye.svg';
|
||||
import { stringToColor } from 'lib/format';
|
||||
import { dateFormat } from 'lib/date';
|
||||
import { safeDecodeURI } from 'next-basics';
|
||||
import styles from './RealtimeLog.module.css';
|
||||
|
||||
const TYPE_ALL = 0;
|
||||
@ -36,7 +37,7 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
|
||||
const logs = useMemo(() => {
|
||||
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) {
|
||||
return logs.filter(row => getType(row) === filter);
|
||||
}
|
||||
@ -44,8 +45,8 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
}, [data, filter]);
|
||||
|
||||
const uuids = useMemo(() => {
|
||||
return data.sessions.reduce((obj, { session_id, session_uuid }) => {
|
||||
obj[session_id] = session_uuid;
|
||||
return data.sessions.reduce((obj, { sessionId, sessionUuid }) => {
|
||||
obj[sessionId] = sessionUuid;
|
||||
return obj;
|
||||
}, {});
|
||||
}, [data]);
|
||||
@ -69,14 +70,14 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
},
|
||||
];
|
||||
|
||||
function getType({ view_id, session_id, event_id }) {
|
||||
if (event_id) {
|
||||
function getType({ pageviewId, sessionId, eventId }) {
|
||||
if (eventId) {
|
||||
return TYPE_EVENT;
|
||||
}
|
||||
if (view_id) {
|
||||
if (pageviewId) {
|
||||
return TYPE_PAGEVIEW;
|
||||
}
|
||||
if (session_id) {
|
||||
if (sessionId) {
|
||||
return TYPE_SESSION;
|
||||
}
|
||||
return null;
|
||||
@ -86,26 +87,26 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
return TYPE_ICONS[getType(row)];
|
||||
}
|
||||
|
||||
function getWebsite({ website_id }) {
|
||||
return websites.find(n => n.website_id === website_id);
|
||||
function getWebsite({ websiteId }) {
|
||||
return websites.find(n => n.id === websiteId);
|
||||
}
|
||||
|
||||
function getDetail({
|
||||
event_name,
|
||||
view_id,
|
||||
session_id,
|
||||
eventName,
|
||||
pageviewId,
|
||||
sessionId,
|
||||
url,
|
||||
browser,
|
||||
os,
|
||||
country,
|
||||
device,
|
||||
website_id,
|
||||
websiteId,
|
||||
}) {
|
||||
if (event_name) {
|
||||
return <div>{event_name}</div>;
|
||||
if (eventName) {
|
||||
return <div>{eventName}</div>;
|
||||
}
|
||||
if (view_id) {
|
||||
const domain = getWebsite({ website_id })?.domain;
|
||||
if (pageviewId) {
|
||||
const domain = getWebsite({ websiteId })?.domain;
|
||||
return (
|
||||
<a
|
||||
className={styles.link}
|
||||
@ -113,11 +114,11 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
target="_blank"
|
||||
rel="noreferrer noopener"
|
||||
>
|
||||
{url}
|
||||
{safeDecodeURI(url)}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
if (session_id) {
|
||||
if (sessionId) {
|
||||
return (
|
||||
<FormattedMessage
|
||||
id="message.log.visitor"
|
||||
@ -133,14 +134,14 @@ export default function RealtimeLog({ data, websites, websiteId }) {
|
||||
}
|
||||
}
|
||||
|
||||
function getTime({ created_at }) {
|
||||
return dateFormat(new Date(created_at), 'pp', locale);
|
||||
function getTime({ createdAt }) {
|
||||
return dateFormat(new Date(createdAt), 'pp', locale);
|
||||
}
|
||||
|
||||
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 }) => {
|
||||
|
@ -16,7 +16,7 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
id =>
|
||||
websites.length === 1
|
||||
? websites[0]?.domain
|
||||
: websites.find(({ website_id }) => website_id === id)?.domain,
|
||||
: websites.find(({ websiteId }) => websiteId === id)?.domain,
|
||||
[websites],
|
||||
);
|
||||
|
||||
@ -65,10 +65,10 @@ export default function RealtimeViews({ websiteId, data, websites }) {
|
||||
|
||||
const pages = percentFilter(
|
||||
pageviews
|
||||
.reduce((arr, { url, website_id }) => {
|
||||
.reduce((arr, { url, websiteId }) => {
|
||||
if (url?.startsWith('/')) {
|
||||
if (!websiteId && websites.length > 1) {
|
||||
url = `${getDomain(website_id)}${url}`;
|
||||
url = `${getDomain(websiteId)}${url}`;
|
||||
}
|
||||
const row = arr.find(({ x }) => x === url);
|
||||
|
||||
|
@ -35,7 +35,7 @@ export default function WebsiteChart({
|
||||
const { get } = useApi();
|
||||
|
||||
const { data, loading, error } = useFetch(
|
||||
`/website/${websiteId}/pageviews`,
|
||||
`/websites/${websiteId}/pageviews`,
|
||||
{
|
||||
params: {
|
||||
start_at: +startDate,
|
||||
@ -70,9 +70,9 @@ export default function WebsiteChart({
|
||||
|
||||
async function handleDateChange(value) {
|
||||
if (value === 'all') {
|
||||
const { data, ok } = await get(`/website/${websiteId}`);
|
||||
const { data, ok } = await get(`/websites/${websiteId}`);
|
||||
if (ok) {
|
||||
setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) });
|
||||
setDateRange({ value, ...getDateRangeValues(new Date(data.createdAt), Date.now()) });
|
||||
}
|
||||
} else {
|
||||
setDateRange(value);
|
||||
|
@ -1,14 +1,13 @@
|
||||
import React from 'react';
|
||||
import Arrow from 'assets/arrow-right.svg';
|
||||
import classNames from 'classnames';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Favicon from 'components/common/Favicon';
|
||||
import Link from 'components/common/Link';
|
||||
import OverflowText from 'components/common/OverflowText';
|
||||
import PageHeader from 'components/layout/PageHeader';
|
||||
import RefreshButton from 'components/common/RefreshButton';
|
||||
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 Arrow from 'assets/arrow-right.svg';
|
||||
import styles from './WebsiteHeader.module.css';
|
||||
|
||||
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} />
|
||||
<Link
|
||||
className={styles.titleLink}
|
||||
href="/website/[...id]"
|
||||
as={`/website/${websiteId}/${title}`}
|
||||
href="/websites/[...id]"
|
||||
as={`/websites/${websiteId}/${title}`}
|
||||
>
|
||||
<OverflowText tooltipId={`${websiteId}-title`}>{title}</OverflowText>
|
||||
</Link>
|
||||
@ -41,8 +40,8 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
|
||||
<RefreshButton websiteId={websiteId} />
|
||||
{showLink && (
|
||||
<Link
|
||||
href="/website/[...id]"
|
||||
as={`/website/${websiteId}/${title}`}
|
||||
href="/websites/[...id]"
|
||||
as={`/websites/${websiteId}/${title}`}
|
||||
className={styles.link}
|
||||
icon={<Arrow />}
|
||||
size="small"
|
||||
|
@ -24,7 +24,7 @@ export default function DashboardEdit({ websites }) {
|
||||
const ordered = useMemo(
|
||||
() =>
|
||||
websites
|
||||
.map(website => ({ ...website, order: order.indexOf(website.website_id) }))
|
||||
.map(website => ({ ...website, order: order.indexOf(website.websiteId) }))
|
||||
.sort(firstBy('order')),
|
||||
[websites, order],
|
||||
);
|
||||
@ -36,7 +36,7 @@ export default function DashboardEdit({ websites }) {
|
||||
const [removed] = orderedWebsites.splice(source.index, 1);
|
||||
orderedWebsites.splice(destination.index, 0, removed);
|
||||
|
||||
setOrder(orderedWebsites.map((website) => website?.website_id || 0));
|
||||
setOrder(orderedWebsites.map(website => website?.websiteId || 0));
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
@ -76,8 +76,8 @@ export default function DashboardEdit({ websites }) {
|
||||
ref={provided.innerRef}
|
||||
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
|
||||
>
|
||||
{ordered.map(({ website_id, name, domain }, index) => (
|
||||
<Draggable key={website_id} draggableId={`${dragId}-${website_id}`} index={index}>
|
||||
{ordered.map(({ websiteId, name, domain }, index) => (
|
||||
<Draggable key={websiteId} draggableId={`${dragId}-${websiteId}`} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
|
@ -21,11 +21,11 @@ function mergeData(state, data, time) {
|
||||
const ids = state.map(({ __id }) => __id);
|
||||
return state
|
||||
.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) {
|
||||
return data.filter(({ website_id }) => website_id === id);
|
||||
return data.filter(({ websiteId }) => websiteId === id);
|
||||
}
|
||||
|
||||
export default function RealtimeDashboard() {
|
||||
|
@ -30,7 +30,7 @@ export default function Settings() {
|
||||
{
|
||||
label: <FormattedMessage id="label.accounts" defaultMessage="Accounts" />,
|
||||
value: ACCOUNTS,
|
||||
hidden: !user?.is_admin,
|
||||
hidden: !user?.isAdmin,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||
|
@ -24,9 +24,9 @@ export default function TestConsole() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const options = data.map(({ name, website_id }) => ({ label: name, value: website_id }));
|
||||
const website = data.find(({ website_id }) => website_id === +websiteId);
|
||||
const selectedValue = options.find(({ value }) => value === website?.website_id)?.value;
|
||||
const options = data.map(({ name, websiteUuid }) => ({ label: name, value: websiteUuid }));
|
||||
const website = data.find(({ websiteUuid }) => websiteId === websiteUuid);
|
||||
const selectedValue = options.find(({ value }) => value === website?.websiteUuid)?.value;
|
||||
|
||||
function handleSelect(value) {
|
||||
router.push(`/console/${value}`);
|
||||
@ -46,7 +46,7 @@ export default function TestConsole() {
|
||||
<script
|
||||
async
|
||||
defer
|
||||
data-website-id={website.website_uuid}
|
||||
data-website-id={website.websiteUuid}
|
||||
src={`${basePath}/umami.js`}
|
||||
data-cache="true"
|
||||
/>
|
||||
@ -104,13 +104,13 @@ export default function TestConsole() {
|
||||
<div className="row">
|
||||
<div className="col-12">
|
||||
<WebsiteChart
|
||||
websiteId={website.website_id}
|
||||
websiteId={website.websiteUuid}
|
||||
title={website.name}
|
||||
domain={website.domain}
|
||||
showLink
|
||||
/>
|
||||
<PageHeader>Events</PageHeader>
|
||||
<EventsChart websiteId={website.website_id} />
|
||||
<EventsChart websiteId={website.websiteUuid} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -24,6 +24,7 @@ import useFetch from 'hooks/useFetch';
|
||||
import usePageQuery from 'hooks/usePageQuery';
|
||||
import { DEFAULT_ANIMATION_DURATION } from 'lib/constants';
|
||||
import styles from './WebsiteDetails.module.css';
|
||||
import EventDataButton from 'components/common/EventDataButton';
|
||||
|
||||
const messages = defineMessages({
|
||||
pages: { id: 'metrics.pages', defaultMessage: 'Pages' },
|
||||
@ -52,7 +53,7 @@ const views = {
|
||||
};
|
||||
|
||||
export default function WebsiteDetails({ websiteId }) {
|
||||
const { data } = useFetch(`/website/${websiteId}`);
|
||||
const { data } = useFetch(`/websites/${websiteId}`);
|
||||
const [chartLoaded, setChartLoaded] = useState(false);
|
||||
const [countryData, setCountryData] = useState();
|
||||
const [eventsData, setEventsData] = useState();
|
||||
@ -183,6 +184,7 @@ export default function WebsiteDetails({ websiteId }) {
|
||||
<EventsTable {...tableProps} onDataLoad={setEventsData} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} md={12} lg={8}>
|
||||
<EventDataButton websiteId={websiteId} />
|
||||
<EventsChart className={styles.eventschart} websiteId={websiteId} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
|
@ -27,7 +27,7 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||
const ordered = useMemo(
|
||||
() =>
|
||||
websites
|
||||
.map(website => ({ ...website, order: websiteOrder.indexOf(website.website_id) || 0 }))
|
||||
.map(website => ({ ...website, order: websiteOrder.indexOf(website.websiteUuid) || 0 }))
|
||||
.sort(firstBy('order')),
|
||||
[websites, websiteOrder],
|
||||
);
|
||||
@ -46,11 +46,11 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ordered.map(({ website_id, name, domain }, index) =>
|
||||
{ordered.map(({ websiteUuid, name, domain }, index) =>
|
||||
index < limit ? (
|
||||
<div key={website_id} className={styles.website}>
|
||||
<div key={websiteUuid} className={styles.website}>
|
||||
<WebsiteChart
|
||||
websiteId={website_id}
|
||||
websiteId={websiteUuid}
|
||||
title={name}
|
||||
domain={domain}
|
||||
showChart={showCharts}
|
||||
|
@ -27,10 +27,10 @@ export default function AccountSettings() {
|
||||
const [message, setMessage] = useState();
|
||||
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 => (
|
||||
<Link href={`/dashboard/${row.user_id}/${row.username}`}>
|
||||
<Link href={`/dashboard/${row.userId}/${row.username}`}>
|
||||
<a>
|
||||
<Icon icon={<LinkIcon />} />
|
||||
</a>
|
||||
@ -42,7 +42,7 @@ export default function AccountSettings() {
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
{!row.is_admin && (
|
||||
{!row.isAdmin && (
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
@ -57,7 +57,7 @@ export default function AccountSettings() {
|
||||
className: 'col-12 col-lg-4',
|
||||
},
|
||||
{
|
||||
key: 'is_admin',
|
||||
key: 'isAdmin',
|
||||
label: <FormattedMessage id="label.administrator" defaultMessage="Administrator" />,
|
||||
className: 'col-12 col-lg-3',
|
||||
render: Checkmark,
|
||||
@ -121,7 +121,7 @@ export default function AccountSettings() {
|
||||
title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'account', id: deleteAccount.user_id, name: deleteAccount.username }}
|
||||
values={{ type: 'accounts', id: deleteAccount.id, name: deleteAccount.username }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
|
@ -32,7 +32,7 @@ export default function ProfileSettings() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { user_id, username } = user;
|
||||
const { userId, username } = user;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -79,7 +79,7 @@ export default function ProfileSettings() {
|
||||
title={<FormattedMessage id="label.change-password" defaultMessage="Change password" />}
|
||||
>
|
||||
<ChangePasswordForm
|
||||
values={{ user_id }}
|
||||
values={{ userId }}
|
||||
onSave={handleSave}
|
||||
onClose={() => setChangePassword(false)}
|
||||
/>
|
||||
|
@ -25,7 +25,11 @@ export default function UserButton() {
|
||||
value: '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' },
|
||||
];
|
||||
|
||||
|
@ -37,16 +37,16 @@ export default function WebsiteSettings() {
|
||||
const [saved, setSaved] = useState(0);
|
||||
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 => (
|
||||
<ButtonLayout align="right">
|
||||
{row.share_id && (
|
||||
{row.shareId && (
|
||||
<Button
|
||||
icon={<LinkIcon />}
|
||||
size="small"
|
||||
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)}
|
||||
/>
|
||||
)}
|
||||
@ -56,46 +56,46 @@ export default function WebsiteSettings() {
|
||||
tooltip={
|
||||
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
|
||||
}
|
||||
tooltipId={`button-code-${row.website_id}`}
|
||||
tooltipId={`button-code-${row.websiteUuid}`}
|
||||
onClick={() => setShowCode(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Pen />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.edit" defaultMessage="Edit" />}
|
||||
tooltipId={`button-edit-${row.website_id}`}
|
||||
tooltipId={`button-edit-${row.websiteUuid}`}
|
||||
onClick={() => setEditWebsite(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Reset />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.reset" defaultMessage="Reset" />}
|
||||
tooltipId={`button-reset-${row.website_id}`}
|
||||
tooltipId={`button-reset-${row.websiteUuid}`}
|
||||
onClick={() => setResetWebsite(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Trash />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.delete" defaultMessage="Delete" />}
|
||||
tooltipId={`button-delete-${row.website_id}`}
|
||||
tooltipId={`button-delete-${row.websiteUuid}`}
|
||||
onClick={() => setDeleteWebsite(row)}
|
||||
/>
|
||||
</ButtonLayout>
|
||||
);
|
||||
|
||||
const DetailsLink = ({ website_id, name, domain }) => (
|
||||
const DetailsLink = ({ websiteUuid, name, domain }) => (
|
||||
<Link
|
||||
className={styles.detailLink}
|
||||
href="/website/[...id]"
|
||||
as={`/website/${website_id}/${name}`}
|
||||
href="/websites/[...id]"
|
||||
as={`/websites/${websiteUuid}/${name}`}
|
||||
>
|
||||
<Favicon domain={domain} />
|
||||
<OverflowText tooltipId={`${website_id}-name`}>{name}</OverflowText>
|
||||
<OverflowText tooltipId={`${websiteUuid}-name`}>{name}</OverflowText>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const Domain = ({ domain, website_id }) => (
|
||||
<OverflowText tooltipId={`${website_id}-domain`}>{domain}</OverflowText>
|
||||
const Domain = ({ domain, websiteUuid }) => (
|
||||
<OverflowText tooltipId={`${websiteUuid}-domain`}>{domain}</OverflowText>
|
||||
);
|
||||
|
||||
const adminColumns = [
|
||||
@ -187,7 +187,7 @@ export default function WebsiteSettings() {
|
||||
<FormattedMessage id="label.add-website" defaultMessage="Add website" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={user.is_admin ? adminColumns : columns} rows={data} empty={empty} />
|
||||
<Table columns={user.isAdmin ? adminColumns : columns} rows={data} empty={empty} />
|
||||
{editWebsite && (
|
||||
<Modal title={<FormattedMessage id="label.edit-website" defaultMessage="Edit website" />}>
|
||||
<WebsiteEditForm values={editWebsite} onSave={handleSave} onClose={handleClose} />
|
||||
@ -203,7 +203,7 @@ export default function WebsiteSettings() {
|
||||
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
|
||||
>
|
||||
<ResetForm
|
||||
values={{ type: 'website', id: resetWebsite.website_id, name: resetWebsite.name }}
|
||||
values={{ type: 'websites', id: resetWebsite.websiteUuid, name: resetWebsite.name }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
@ -214,7 +214,7 @@ export default function WebsiteSettings() {
|
||||
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'website', id: deleteWebsite.website_id, name: deleteWebsite.name }}
|
||||
values={{ type: 'websites', id: deleteWebsite.websiteUuid, name: deleteWebsite.name }}
|
||||
onSave={handleSave}
|
||||
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;
|
70
db/clickhouse/schema.sql
Normal file
70
db/clickhouse/schema.sql
Normal file
@ -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;
|
35
db/mysql/migrations/04_add_uuid/migration.sql
Normal file
35
db/mysql/migrations/04_add_uuid/migration.sql
Normal file
@ -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,87 +8,96 @@ datasource db {
|
||||
}
|
||||
|
||||
model account {
|
||||
user_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
username String @unique() @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
is_admin Boolean @default(false)
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
updated_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
website website[]
|
||||
id Int @id @default(autoincrement()) @map("user_id") @db.UnsignedInt
|
||||
username String @unique() @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
isAdmin Boolean @default(false) @map("is_admin")
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamp(0)
|
||||
accountUuid String @unique() @map("account_uuid") @db.VarChar(36)
|
||||
website website[]
|
||||
|
||||
@@index([accountUuid])
|
||||
}
|
||||
|
||||
model event {
|
||||
event_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
website_id Int @db.UnsignedInt
|
||||
session_id Int @db.UnsignedInt
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
url String @db.VarChar(500)
|
||||
event_name String @db.VarChar(50)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
event_data event_data?
|
||||
id Int @id @default(autoincrement()) @map("event_id") @db.UnsignedInt
|
||||
websiteId Int @map("website_id") @db.UnsignedInt
|
||||
sessionId Int @map("session_id") @db.UnsignedInt
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
url String @db.VarChar(500)
|
||||
eventName String @map("event_name") @db.VarChar(50)
|
||||
eventUuid String @unique() @map("event_uuid") @db.VarChar(36)
|
||||
session session @relation(fields: [sessionId], references: [id])
|
||||
website website @relation(fields: [websiteId], references: [id])
|
||||
eventData eventData?
|
||||
|
||||
@@index([created_at])
|
||||
@@index([session_id])
|
||||
@@index([website_id])
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([websiteId])
|
||||
@@index([eventUuid])
|
||||
}
|
||||
|
||||
model event_data {
|
||||
event_data_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
event_id Int @unique @db.UnsignedInt
|
||||
event_data Json
|
||||
event event @relation(fields: [event_id], references: [event_id])
|
||||
model eventData {
|
||||
id Int @id @default(autoincrement()) @map("event_data_id") @db.UnsignedInt
|
||||
eventId Int @unique @map("event_id") @db.UnsignedInt
|
||||
eventData Json @map("event_data")
|
||||
event event @relation(fields: [eventId], references: [id])
|
||||
|
||||
@@map("event_data")
|
||||
}
|
||||
|
||||
model pageview {
|
||||
view_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
website_id Int @db.UnsignedInt
|
||||
session_id Int @db.UnsignedInt
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
url String @db.VarChar(500)
|
||||
referrer String? @db.VarChar(500)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
id Int @id @default(autoincrement()) @map("view_id") @db.UnsignedInt
|
||||
websiteId Int @map("website_id") @db.UnsignedInt
|
||||
sessionId Int @map("session_id") @db.UnsignedInt
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
url String @db.VarChar(500)
|
||||
referrer String? @db.VarChar(500)
|
||||
session session @relation(fields: [sessionId], references: [id])
|
||||
website website @relation(fields: [websiteId], references: [id])
|
||||
|
||||
@@index([created_at])
|
||||
@@index([session_id])
|
||||
@@index([website_id, created_at])
|
||||
@@index([website_id])
|
||||
@@index([website_id, session_id, created_at])
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([websiteId, createdAt])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
}
|
||||
|
||||
model session {
|
||||
session_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
session_uuid String @unique() @db.VarChar(36)
|
||||
website_id Int @db.UnsignedInt
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
hostname String? @db.VarChar(100)
|
||||
browser String? @db.VarChar(20)
|
||||
os String? @db.VarChar(20)
|
||||
device String? @db.VarChar(20)
|
||||
screen String? @db.VarChar(11)
|
||||
language String? @db.VarChar(35)
|
||||
country String? @db.Char(2)
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
id Int @id @default(autoincrement()) @map("session_id") @db.UnsignedInt
|
||||
sessionUuid String @unique() @map("session_uuid") @db.VarChar(36)
|
||||
websiteId Int @map("website_id") @db.UnsignedInt
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
hostname String? @db.VarChar(100)
|
||||
browser String? @db.VarChar(20)
|
||||
os String? @db.VarChar(20)
|
||||
device String? @db.VarChar(20)
|
||||
screen String? @db.VarChar(11)
|
||||
language String? @db.VarChar(35)
|
||||
country String? @db.Char(2)
|
||||
website website @relation(fields: [websiteId], references: [id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
|
||||
@@index([created_at])
|
||||
@@index([website_id])
|
||||
@@index([createdAt])
|
||||
@@index([websiteId])
|
||||
@@index([sessionUuid])
|
||||
}
|
||||
|
||||
model website {
|
||||
website_id Int @id @default(autoincrement()) @db.UnsignedInt
|
||||
website_uuid String @unique() @db.VarChar(36)
|
||||
user_id Int @db.UnsignedInt
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
share_id String? @unique() @db.VarChar(64)
|
||||
created_at DateTime? @default(now()) @db.Timestamp(0)
|
||||
account account @relation(fields: [user_id], references: [user_id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
session session[]
|
||||
id Int @id @default(autoincrement()) @map("website_id") @db.UnsignedInt
|
||||
websiteUuid String @unique() @map("website_uuid") @db.VarChar(36)
|
||||
userId Int @map("user_id") @db.UnsignedInt
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
shareId String? @unique() @map("share_id") @db.VarChar(64)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamp(0)
|
||||
account account @relation(fields: [userId], references: [id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
session session[]
|
||||
|
||||
@@index([user_id])
|
||||
@@index([userId])
|
||||
@@index([websiteUuid])
|
||||
}
|
||||
|
36
db/postgresql/migrations/04_add_uuid/migration.sql
Normal file
36
db/postgresql/migrations/04_add_uuid/migration.sql
Normal file
@ -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,87 +8,96 @@ datasource db {
|
||||
}
|
||||
|
||||
model account {
|
||||
user_id Int @id @default(autoincrement())
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
is_admin Boolean @default(false)
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
updated_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
website website[]
|
||||
id Int @id @default(autoincrement()) @map("user_id")
|
||||
username String @unique @db.VarChar(255)
|
||||
password String @db.VarChar(60)
|
||||
isAdmin Boolean @default(false) @map("is_admin")
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
updatedAt DateTime? @default(now()) @map("updated_at") @db.Timestamptz(6)
|
||||
accountUuid String @unique @map("account_uuid") @db.Uuid
|
||||
website website[]
|
||||
|
||||
@@index([accountUuid])
|
||||
}
|
||||
|
||||
model event {
|
||||
event_id Int @id @default(autoincrement())
|
||||
website_id Int
|
||||
session_id Int
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
url String @db.VarChar(500)
|
||||
event_name String @db.VarChar(50)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
event_data event_data?
|
||||
id Int @id() @default(autoincrement()) @map("event_id")
|
||||
websiteId Int @map("website_id")
|
||||
sessionId Int @map("session_id")
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
url String @db.VarChar(500)
|
||||
eventName String @map("event_name") @db.VarChar(50)
|
||||
eventUuid String @unique @map("event_uuid") @db.Uuid
|
||||
session session @relation(fields: [sessionId], references: [id])
|
||||
website website @relation(fields: [websiteId], references: [id])
|
||||
eventData eventData?
|
||||
|
||||
@@index([created_at])
|
||||
@@index([session_id])
|
||||
@@index([website_id])
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([websiteId])
|
||||
@@index([eventUuid])
|
||||
}
|
||||
|
||||
model event_data {
|
||||
event_data_id Int @id @default(autoincrement())
|
||||
event_id Int @unique
|
||||
event_data Json
|
||||
event event @relation(fields: [event_id], references: [event_id])
|
||||
model eventData {
|
||||
id Int @id @default(autoincrement()) @map("event_data_id")
|
||||
eventId Int @unique @map("event_id")
|
||||
eventData Json @map("event_data")
|
||||
event event @relation(fields: [eventId], references: [id])
|
||||
|
||||
@@map("event_data")
|
||||
}
|
||||
|
||||
model pageview {
|
||||
view_id Int @id @default(autoincrement())
|
||||
website_id Int
|
||||
session_id Int
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
url String @db.VarChar(500)
|
||||
referrer String? @db.VarChar(500)
|
||||
session session @relation(fields: [session_id], references: [session_id])
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
id Int @id @default(autoincrement()) @map("view_id")
|
||||
websiteId Int @map("website_id")
|
||||
sessionId Int @map("session_id")
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
url String @db.VarChar(500)
|
||||
referrer String? @db.VarChar(500)
|
||||
session session @relation(fields: [sessionId], references: [id])
|
||||
website website @relation(fields: [websiteId], references: [id])
|
||||
|
||||
@@index([created_at])
|
||||
@@index([session_id])
|
||||
@@index([website_id, created_at])
|
||||
@@index([website_id])
|
||||
@@index([website_id, session_id, created_at])
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([websiteId, createdAt])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
}
|
||||
|
||||
model session {
|
||||
session_id Int @id @default(autoincrement())
|
||||
session_uuid String @unique @db.Uuid
|
||||
website_id Int
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
hostname String? @db.VarChar(100)
|
||||
browser String? @db.VarChar(20)
|
||||
os String? @db.VarChar(20)
|
||||
device String? @db.VarChar(20)
|
||||
screen String? @db.VarChar(11)
|
||||
language String? @db.VarChar(35)
|
||||
country String? @db.Char(2)
|
||||
website website @relation(fields: [website_id], references: [website_id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
id Int @id @default(autoincrement()) @map("session_id")
|
||||
sessionUuid String @unique @map("session_uuid") @db.Uuid
|
||||
websiteId Int @map("website_id")
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
hostname String? @db.VarChar(100)
|
||||
browser String? @db.VarChar(20)
|
||||
os String? @db.VarChar(20)
|
||||
device String? @db.VarChar(20)
|
||||
screen String? @db.VarChar(11)
|
||||
language String? @db.VarChar(35)
|
||||
country String? @db.Char(2)
|
||||
website website? @relation(fields: [websiteId], references: [id])
|
||||
events event[]
|
||||
pageview pageview[]
|
||||
|
||||
@@index([created_at])
|
||||
@@index([website_id])
|
||||
@@index([createdAt])
|
||||
@@index([websiteId])
|
||||
@@index([sessionUuid])
|
||||
}
|
||||
|
||||
model website {
|
||||
website_id Int @id @default(autoincrement())
|
||||
website_uuid String @unique @db.Uuid
|
||||
user_id Int
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
share_id String? @unique @db.VarChar(64)
|
||||
created_at DateTime? @default(now()) @db.Timestamptz(6)
|
||||
account account @relation(fields: [user_id], references: [user_id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
session session[]
|
||||
id Int @id @default(autoincrement()) @map("website_id")
|
||||
websiteUuid String @unique @map("website_uuid") @db.Uuid
|
||||
userId Int @map("user_id")
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
shareId String? @unique @map("share_id") @db.VarChar(64)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
account account @relation(fields: [userId], references: [id])
|
||||
event event[]
|
||||
pageview pageview[]
|
||||
session session[]
|
||||
|
||||
@@index([user_id])
|
||||
@@index([userId])
|
||||
@@index([websiteUuid])
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
{
|
||||
"label.accounts": "Accounts",
|
||||
"label.add-account": "Add account",
|
||||
"label.add-column": "Add column",
|
||||
"label.add-filter": "Add filter",
|
||||
"label.add-website": "Add website",
|
||||
"label.administrator": "Administrator",
|
||||
"label.all": "All",
|
||||
@ -25,6 +27,8 @@
|
||||
"label.edit-account": "Edit account",
|
||||
"label.edit-website": "Edit website",
|
||||
"label.enable-share-url": "Enable share URL",
|
||||
"label.event-data": "Event Data",
|
||||
"label.field-name": "Field Name",
|
||||
"label.invalid": "Invalid",
|
||||
"label.invalid-domain": "Invalid domain",
|
||||
"label.language": "Language",
|
||||
@ -48,6 +52,7 @@
|
||||
"label.reset": "Reset",
|
||||
"label.reset-website": "Reset statistics",
|
||||
"label.save": "Save",
|
||||
"label.search": "Search",
|
||||
"label.settings": "Settings",
|
||||
"label.share-url": "Share URL",
|
||||
"label.single-day": "Single day",
|
||||
@ -58,8 +63,10 @@
|
||||
"label.timezone": "Timezone",
|
||||
"label.today": "Today",
|
||||
"label.tracking-code": "Tracking code",
|
||||
"label.type": "Type",
|
||||
"label.unknown": "Unknown",
|
||||
"label.username": "Username",
|
||||
"label.value": "Value",
|
||||
"label.view-details": "View details",
|
||||
"label.websites": "Websites",
|
||||
"label.yesterday": "Yesterday",
|
||||
|
@ -62,6 +62,7 @@
|
||||
"label.username": "用户名",
|
||||
"label.view-details": "查看更多",
|
||||
"label.websites": "网站",
|
||||
"label.yesterday": "昨天",
|
||||
"message.active-users": "当前在线 {x} 人",
|
||||
"message.confirm-delete": "你确定要删除 {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 { AUTH_TOKEN, SHARE_TOKEN_HEADER } from './constants';
|
||||
import { getWebsiteById } from 'queries';
|
||||
import { secret } from './crypto';
|
||||
import { parseSecureToken, parseToken } from 'next-basics';
|
||||
import { getWebsite } from 'queries';
|
||||
import { SHARE_TOKEN_HEADER } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
|
||||
export async function getAuthToken(req) {
|
||||
export function getAuthToken(req) {
|
||||
try {
|
||||
const token = req.headers.authorization;
|
||||
|
||||
@ -13,13 +13,15 @@ export async function getAuthToken(req) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getAuthHeader() {
|
||||
const token = getItem(AUTH_TOKEN);
|
||||
|
||||
return token ? { authorization: `Bearer ${token}` } : {};
|
||||
export function getShareToken(req) {
|
||||
try {
|
||||
return parseSecureToken(req.headers[SHARE_TOKEN_HEADER], secret());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function isValidToken(token, validation) {
|
||||
export function isValidToken(token, validation) {
|
||||
try {
|
||||
const result = parseToken(token, secret());
|
||||
|
||||
@ -35,25 +37,23 @@ export async function isValidToken(token, validation) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function allowQuery(req, skipToken) {
|
||||
const { id } = req.query;
|
||||
const token = req.headers[SHARE_TOKEN_HEADER];
|
||||
const websiteId = +id;
|
||||
export async function allowQuery(req) {
|
||||
const { id: websiteId } = req.query;
|
||||
|
||||
const website = await getWebsiteById(websiteId);
|
||||
const { userId, isAdmin, shareToken } = req.auth ?? {};
|
||||
|
||||
if (website) {
|
||||
if (token && token !== 'undefined' && !skipToken) {
|
||||
return isValidToken(token, { website_id: websiteId });
|
||||
}
|
||||
if (isAdmin) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const authToken = await getAuthToken(req);
|
||||
if (shareToken) {
|
||||
return isValidToken(shareToken, { websiteUuid: websiteId });
|
||||
}
|
||||
|
||||
if (authToken) {
|
||||
const { user_id, is_admin } = authToken;
|
||||
if (userId) {
|
||||
const website = await getWebsite({ websiteUuid: websiteId });
|
||||
|
||||
return is_admin || website.user_id === user_id;
|
||||
}
|
||||
return website && website.userId === userId;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -14,6 +14,9 @@ export const CLICKHOUSE_DATE_FORMATS = {
|
||||
|
||||
const log = debug('umami:clickhouse');
|
||||
|
||||
let clickhouse;
|
||||
const enabled = Boolean(process.env.CLICKHOUSE_URL);
|
||||
|
||||
function getClient() {
|
||||
const {
|
||||
hostname,
|
||||
@ -57,12 +60,53 @@ function getDateFormat(date) {
|
||||
return `'${dateFormat(date, 'UTC:yyyy-mm-dd HH:MM:ss')}'`;
|
||||
}
|
||||
|
||||
function getBetweenDates(field, start_at, end_at) {
|
||||
return `${field} between ${getDateFormat(start_at)}
|
||||
and ${getDateFormat(end_at)}`;
|
||||
function getCommaSeparatedStringFormat(data) {
|
||||
return data.map(a => `'${a}'`).join(',') || '';
|
||||
}
|
||||
|
||||
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 filter = filters[key];
|
||||
|
||||
@ -72,48 +116,28 @@ function getFilterQuery(table, column, filters = {}, params = []) {
|
||||
|
||||
switch (key) {
|
||||
case 'url':
|
||||
if (table === 'pageview' || table === 'event') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'os':
|
||||
case 'browser':
|
||||
case 'device':
|
||||
case 'country':
|
||||
if (table === 'session') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'event_name':
|
||||
if (table === 'event') {
|
||||
arr.push(`and ${table}.${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
}
|
||||
arr.push(`and ${key}=$${params.length + 1}`);
|
||||
params.push(decodeURIComponent(filter));
|
||||
break;
|
||||
|
||||
case 'referrer':
|
||||
if (table === 'pageview' || table === 'event') {
|
||||
arr.push(`and ${table}.referrer like $${params.length + 1}`);
|
||||
params.push(`%${decodeURIComponent(filter)}%`);
|
||||
}
|
||||
arr.push(`and referrer like $${params.length + 1}`);
|
||||
params.push(`%${decodeURIComponent(filter)}%`);
|
||||
break;
|
||||
|
||||
case 'domain':
|
||||
if (table === 'pageview') {
|
||||
arr.push(`and ${table}.referrer not like $${params.length + 1}`);
|
||||
arr.push(`and ${table}.referrer not like '/%'`);
|
||||
params.push(`%://${filter}/%`);
|
||||
}
|
||||
arr.push(`and referrer not like $${params.length + 1}`);
|
||||
arr.push(`and referrer not like '/%'`);
|
||||
params.push(`%://${filter}/%`);
|
||||
break;
|
||||
|
||||
case 'query':
|
||||
if (table === 'pageview') {
|
||||
arr.push(`and ${table}.url like '%?%'`);
|
||||
}
|
||||
arr.push(`and url like '%?%'`);
|
||||
}
|
||||
|
||||
return arr;
|
||||
@ -122,7 +146,7 @@ function getFilterQuery(table, column, filters = {}, params = []) {
|
||||
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 } =
|
||||
filters;
|
||||
|
||||
@ -135,13 +159,9 @@ function parseFilters(table, column, filters = {}, params = [], sessionKey = 'se
|
||||
sessionFilters,
|
||||
eventFilters,
|
||||
event: { event_name },
|
||||
joinSession:
|
||||
os || browser || device || country
|
||||
? `inner join session on ${table}.${sessionKey} = session.${sessionKey}`
|
||||
: '',
|
||||
pageviewQuery: getFilterQuery('pageview', column, pageviewFilters, params),
|
||||
sessionQuery: getFilterQuery('session', column, sessionFilters, params),
|
||||
eventQuery: getFilterQuery('event', column, eventFilters, params),
|
||||
pageviewQuery: getFilterQuery(column, pageviewFilters, params),
|
||||
sessionQuery: getFilterQuery(column, sessionFilters, params),
|
||||
eventQuery: getFilterQuery(column, eventFilters, params),
|
||||
};
|
||||
}
|
||||
|
||||
@ -168,6 +188,8 @@ async function rawQuery(query, params = []) {
|
||||
log(formattedQuery);
|
||||
}
|
||||
|
||||
await connect();
|
||||
|
||||
return clickhouse.query(formattedQuery).toPromise();
|
||||
}
|
||||
|
||||
@ -183,16 +205,26 @@ async function findFirst(data) {
|
||||
return data[0] ?? null;
|
||||
}
|
||||
|
||||
// Initialization
|
||||
const clickhouse = process.env.CLICKHOUSE_URL && (global[CLICKHOUSE] || getClient());
|
||||
async function connect() {
|
||||
if (!clickhouse) {
|
||||
clickhouse = process.env.CLICKHOUSE_URL && (global[CLICKHOUSE] || getClient());
|
||||
}
|
||||
|
||||
return clickhouse;
|
||||
}
|
||||
|
||||
export default {
|
||||
enabled,
|
||||
client: clickhouse,
|
||||
log,
|
||||
connect,
|
||||
getDateStringQuery,
|
||||
getDateQuery,
|
||||
getDateFormat,
|
||||
getCommaSeparatedStringFormat,
|
||||
getBetweenDates,
|
||||
getEventDataColumnsQuery,
|
||||
getEventDataFilterQuery,
|
||||
getFilterQuery,
|
||||
parseFilters,
|
||||
findUnique,
|
||||
|
@ -12,7 +12,7 @@ BigInt.prototype.toJSON = function () {
|
||||
};
|
||||
|
||||
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') {
|
||||
return POSTGRESQL;
|
||||
|
@ -74,7 +74,7 @@ export function stringToColor(str) {
|
||||
let color = '#';
|
||||
for (let i = 0; i < 3; i++) {
|
||||
let value = (hash >> (i * 8)) & 0xff;
|
||||
color += ('00' + value.toString(16)).substring(-2);
|
||||
color += ('00' + value.toString(16)).slice(-2);
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
41
lib/kafka.js
41
lib/kafka.js
@ -5,6 +5,10 @@ import { KAFKA, KAFKA_PRODUCER } from 'lib/db';
|
||||
|
||||
const log = debug('umami:kafka');
|
||||
|
||||
let kafka;
|
||||
let producer;
|
||||
const enabled = Boolean(process.env.KAFKA_URL && process.env.KAFKA_BROKER);
|
||||
|
||||
function getClient() {
|
||||
const { username, password } = new URL(process.env.KAFKA_URL);
|
||||
const brokers = process.env.KAFKA_BROKER.split(',');
|
||||
@ -12,7 +16,12 @@ function getClient() {
|
||||
const ssl =
|
||||
username && password
|
||||
? {
|
||||
ssl: true,
|
||||
ssl: {
|
||||
checkServerIdentity: () => undefined,
|
||||
ca: [process.env.CA_CERT],
|
||||
key: process.env.CLIENT_KEY,
|
||||
cert: process.env.CLIENT_CERT,
|
||||
},
|
||||
sasl: {
|
||||
mechanism: 'plain',
|
||||
username,
|
||||
@ -33,6 +42,8 @@ function getClient() {
|
||||
global[KAFKA] = client;
|
||||
}
|
||||
|
||||
log('Kafka initialized');
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
@ -44,6 +55,8 @@ async function getProducer() {
|
||||
global[KAFKA_PRODUCER] = producer;
|
||||
}
|
||||
|
||||
log('Kafka producer initialized');
|
||||
|
||||
return producer;
|
||||
}
|
||||
|
||||
@ -52,6 +65,8 @@ function getDateFormat(date) {
|
||||
}
|
||||
|
||||
async function sendMessage(params, topic) {
|
||||
await connect();
|
||||
|
||||
await producer.send({
|
||||
topic,
|
||||
messages: [
|
||||
@ -59,26 +74,28 @@ async function sendMessage(params, topic) {
|
||||
value: JSON.stringify(params),
|
||||
},
|
||||
],
|
||||
acks: 0,
|
||||
acks: 1,
|
||||
});
|
||||
}
|
||||
|
||||
// Initialization
|
||||
let kafka;
|
||||
let producer;
|
||||
async function connect() {
|
||||
if (!kafka) {
|
||||
kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (global[KAFKA] || getClient());
|
||||
|
||||
(async () => {
|
||||
kafka = process.env.KAFKA_URL && process.env.KAFKA_BROKER && (global[KAFKA] || getClient());
|
||||
|
||||
if (kafka) {
|
||||
producer = global[KAFKA_PRODUCER] || (await getProducer());
|
||||
if (kafka) {
|
||||
producer = global[KAFKA_PRODUCER] || (await getProducer());
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
return kafka;
|
||||
}
|
||||
|
||||
export default {
|
||||
enabled,
|
||||
client: kafka,
|
||||
producer: producer,
|
||||
producer,
|
||||
log,
|
||||
connect,
|
||||
getDateFormat,
|
||||
sendMessage,
|
||||
};
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { createMiddleware, unauthorized, badRequest, serverError } from 'next-basics';
|
||||
import cors from 'cors';
|
||||
import { getSession } from './session';
|
||||
import { getAuthToken } from './auth';
|
||||
import { getAuthToken, getShareToken } from './auth';
|
||||
|
||||
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) => {
|
||||
const token = await getAuthToken(req);
|
||||
const shareToken = await getShareToken(req);
|
||||
|
||||
if (!token) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
req.auth = token;
|
||||
req.auth = { ...token, shareToken };
|
||||
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 = []) {
|
||||
const query = Object.keys(filters).reduce((arr, key) => {
|
||||
const filter = filters[key];
|
||||
@ -193,6 +251,8 @@ export default {
|
||||
getDateQuery,
|
||||
getTimestampInterval,
|
||||
getFilterQuery,
|
||||
getEventDataColumnsQuery,
|
||||
getEventDataFilterQuery,
|
||||
parseFilters,
|
||||
rawQuery,
|
||||
transaction,
|
||||
|
45
lib/redis.js
45
lib/redis.js
@ -8,12 +8,20 @@ const log = debug('umami:redis');
|
||||
const INITIALIZED = 'redis:initialized';
|
||||
export const DELETED = 'deleted';
|
||||
|
||||
let redis;
|
||||
const enabled = Boolean(process.env.REDIS_URL);
|
||||
|
||||
function getClient() {
|
||||
if (!process.env.REDIS_URL) {
|
||||
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') {
|
||||
global[REDIS] = redis;
|
||||
@ -29,32 +37,43 @@ async function stageData() {
|
||||
const websites = await getAllWebsites();
|
||||
|
||||
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 => {
|
||||
return { key: `website:${a.website_uuid}`, value: Number(a.website_id) };
|
||||
return { key: `website:${a.websiteUuid}`, value: Number(a.websiteId) };
|
||||
});
|
||||
|
||||
await addRedis(sessionUuids);
|
||||
await addRedis(websiteIds);
|
||||
await addSet(sessionUuids);
|
||||
await addSet(websiteIds);
|
||||
|
||||
await redis.set(INITIALIZED, 1);
|
||||
}
|
||||
|
||||
async function addRedis(ids) {
|
||||
async function addSet(ids) {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const { key, value } = ids[i];
|
||||
await redis.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Initialization
|
||||
const redis = process.env.REDIS_URL && (global[REDIS] || getClient());
|
||||
async function get(key) {
|
||||
await connect();
|
||||
|
||||
(async () => {
|
||||
if (redis && !(await redis.get(INITIALIZED))) {
|
||||
await stageData();
|
||||
return redis.get(key);
|
||||
}
|
||||
|
||||
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());
|
||||
}
|
||||
})();
|
||||
|
||||
export default { client: redis, stageData, log };
|
||||
return redis;
|
||||
}
|
||||
|
||||
export default { enabled, client: redis, log, connect, get, set, stageData };
|
||||
|
@ -1,7 +1,8 @@
|
||||
import { parseToken } from 'next-basics';
|
||||
import { validate } from 'uuid';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { secret, uuid } from 'lib/crypto';
|
||||
import redis, { DELETED } from 'lib/redis';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { getClientInfo, getJsonBody } from 'lib/request';
|
||||
import { createSession, getSessionByUuid, getWebsiteByUuid } from 'queries';
|
||||
|
||||
@ -15,80 +16,91 @@ export async function getSession(req) {
|
||||
const cache = req.headers['x-umami-cache'];
|
||||
|
||||
if (cache) {
|
||||
const result = await parseToken(cache);
|
||||
const result = await parseToken(cache, secret());
|
||||
|
||||
if (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;
|
||||
}
|
||||
|
||||
let websiteId = null;
|
||||
|
||||
// Check if website exists
|
||||
if (redis.client) {
|
||||
websiteId = await redis.client.get(`website:${website_uuid}`);
|
||||
if (redis.enabled) {
|
||||
websiteId = Number(await redis.get(`website:${websiteUuid}`));
|
||||
}
|
||||
|
||||
// Check database if redis does not have
|
||||
// Check database if does not exists in Redis
|
||||
if (!websiteId) {
|
||||
const website = await getWebsiteByUuid(website_uuid);
|
||||
websiteId = website ? website.website_id : null;
|
||||
const website = await getWebsiteByUuid(websiteUuid);
|
||||
websiteId = website ? website.id : null;
|
||||
}
|
||||
|
||||
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 sessionUuid = uuid(websiteUuid, hostname, ip, userAgent);
|
||||
|
||||
const session_uuid = uuid(websiteId, hostname, ip, userAgent);
|
||||
|
||||
let sessionCreated = false;
|
||||
let sessionId = null;
|
||||
let session = null;
|
||||
|
||||
// Check if session exists
|
||||
if (redis.client) {
|
||||
sessionCreated = !!(await redis.client.get(`session:${session_uuid}`));
|
||||
}
|
||||
if (!clickhouse.enabled) {
|
||||
// Check if session exists
|
||||
if (redis.enabled) {
|
||||
sessionId = Number(await redis.get(`session:${sessionUuid}`));
|
||||
}
|
||||
|
||||
// Check database if redis does not have
|
||||
if (!sessionCreated) {
|
||||
session = await getSessionByUuid(session_uuid);
|
||||
sessionCreated = !!session;
|
||||
sessionId = session ? session.session_id : null;
|
||||
}
|
||||
// Check database if does not exists in Redis
|
||||
if (!sessionId) {
|
||||
session = await getSessionByUuid(sessionUuid);
|
||||
sessionId = session ? session.id : null;
|
||||
}
|
||||
|
||||
if (!sessionCreated) {
|
||||
try {
|
||||
session = await createSession(websiteId, {
|
||||
session_uuid,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
device,
|
||||
});
|
||||
|
||||
sessionId = session ? session.session_id : null;
|
||||
} catch (e) {
|
||||
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||
throw e;
|
||||
if (!sessionId) {
|
||||
try {
|
||||
session = await createSession(websiteId, {
|
||||
sessionUuid,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
device,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
session = {
|
||||
sessionId,
|
||||
sessionUuid,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
device,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
website_id: websiteId,
|
||||
session_id: sessionId,
|
||||
session_uuid,
|
||||
website: {
|
||||
websiteId,
|
||||
websiteUuid,
|
||||
},
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
@ -36,6 +36,7 @@ module.exports = {
|
||||
env: {
|
||||
currentVersion: pkg.version,
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
isCloudMode: process.env.CLOUD_MODE,
|
||||
},
|
||||
basePath: process.env.BASE_PATH,
|
||||
output: 'standalone',
|
||||
|
10
package.json
10
package.json
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "1.38.0",
|
||||
"version": "1.39.0",
|
||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
@ -56,7 +56,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "4.5.7",
|
||||
"@prisma/client": "4.3.1",
|
||||
"@prisma/client": "4.5.0",
|
||||
"chalk": "^4.1.1",
|
||||
"chart.js": "^2.9.4",
|
||||
"classnames": "^2.3.1",
|
||||
@ -83,8 +83,8 @@
|
||||
"kafkajs": "^2.1.0",
|
||||
"maxmind": "^4.3.6",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "^12.2.5",
|
||||
"next-basics": "^0.7.0",
|
||||
"next": "^12.3.1",
|
||||
"next-basics": "^0.18.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prop-types": "^15.7.2",
|
||||
@ -125,7 +125,7 @@
|
||||
"postcss-preset-env": "7.4.3",
|
||||
"postcss-rtlcss": "^3.6.1",
|
||||
"prettier": "^2.6.2",
|
||||
"prisma": "4.3.1",
|
||||
"prisma": "4.5.0",
|
||||
"prompts": "2.4.2",
|
||||
"rollup": "^2.70.1",
|
||||
"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);
|
||||
};
|
66
pages/api/accounts/[id]/index.js
Normal file
66
pages/api/accounts/[id]/index.js
Normal file
@ -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) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
const { user_id: auth_user_id, is_admin } = req.auth;
|
||||
const { user_id, current_password, new_password } = req.body;
|
||||
const { userId: currentUserId, isAdmin: currentUserIsAdmin } = req.auth;
|
||||
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);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const account = await getAccountById(user_id);
|
||||
const valid = checkPassword(current_password, account.password);
|
||||
const account = await getAccountById(userId);
|
||||
|
||||
if (!valid) {
|
||||
if (!checkPassword(current_password, account.password)) {
|
||||
return badRequest(res, 'Current password is incorrect');
|
||||
}
|
||||
|
||||
const password = hashPassword(new_password);
|
||||
|
||||
const updated = await updateAccount(user_id, { password });
|
||||
const updated = await updateAccount(userId, { password });
|
||||
|
||||
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 { ok, unauthorized, methodNotAllowed } from 'next-basics';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { createAccount, getAccountByUsername, getAccounts } from 'queries';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
const { is_admin } = req.auth;
|
||||
const { isAdmin } = req.auth;
|
||||
|
||||
if (!is_admin) {
|
||||
if (!isAdmin) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
@ -17,5 +18,23 @@ export default async (req, res) => {
|
||||
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);
|
||||
};
|
||||
|
@ -12,8 +12,8 @@ export default async (req, res) => {
|
||||
const account = await getAccountByUsername(username);
|
||||
|
||||
if (account && checkPassword(password, account.password)) {
|
||||
const { user_id, username, is_admin } = account;
|
||||
const user = { user_id, username, is_admin };
|
||||
const { id, username, isAdmin, accountUuid } = account;
|
||||
const user = { userId: id, username, isAdmin, accountUuid };
|
||||
const token = createSecureToken(user, secret());
|
||||
|
||||
return ok(res, { token, user });
|
||||
|
@ -58,36 +58,39 @@ export default async (req, res) => {
|
||||
|
||||
await useSession(req, res);
|
||||
|
||||
const {
|
||||
session: { website_id, session_id, session_uuid },
|
||||
} = req;
|
||||
const { website, session } = req.session;
|
||||
|
||||
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) {
|
||||
url = url.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
const event_uuid = uuid();
|
||||
const eventUuid = uuid();
|
||||
|
||||
if (type === 'pageview') {
|
||||
await savePageView(website_id, { session_id, session_uuid, url, referrer });
|
||||
await savePageView(website, { session, url, referrer });
|
||||
} else if (type === 'event') {
|
||||
await saveEvent(website_id, {
|
||||
event_uuid,
|
||||
session_id,
|
||||
session_uuid,
|
||||
await saveEvent(website, {
|
||||
session,
|
||||
eventUuid,
|
||||
url,
|
||||
event_name,
|
||||
event_data,
|
||||
eventName,
|
||||
eventData,
|
||||
});
|
||||
} else {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const token = createToken({ website_id, session_id, session_uuid }, secret());
|
||||
const token = createToken(
|
||||
{
|
||||
website,
|
||||
session,
|
||||
},
|
||||
secret(),
|
||||
);
|
||||
|
||||
return send(res, token);
|
||||
};
|
||||
|
@ -8,10 +8,10 @@ export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { user_id } = req.auth;
|
||||
const { userId } = req.auth;
|
||||
|
||||
const websites = await getUserWebsites(user_id);
|
||||
const ids = websites.map(({ website_id }) => website_id);
|
||||
const websites = await getUserWebsites(userId);
|
||||
const ids = websites.map(({ websiteUuid }) => websiteUuid);
|
||||
const token = createToken({ websites: ids }, secret());
|
||||
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
|
||||
|
||||
|
@ -9,10 +9,10 @@ export default async (req, res) => {
|
||||
const website = await getWebsiteByShareId(id);
|
||||
|
||||
if (website) {
|
||||
const websiteId = website.website_id;
|
||||
const token = createToken({ website_id: websiteId }, secret());
|
||||
const { websiteId, websiteUuid } = website;
|
||||
const token = createToken({ websiteId, websiteUuid }, secret());
|
||||
|
||||
return ok(res, { websiteId, token });
|
||||
return ok(res, { websiteId, websiteUuid, token });
|
||||
}
|
||||
|
||||
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 { allowQuery } from 'lib/auth';
|
||||
import { useCors } from 'lib/middleware';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { getActiveVisitors } from 'queries';
|
||||
|
||||
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))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id } = req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const { id: websiteId } = req.query;
|
||||
|
||||
const result = await getActiveVisitors(websiteId);
|
||||
|
40
pages/api/websites/[id]/eventdata.js
Normal file
40
pages/api/websites/[id]/eventdata.js
Normal file
@ -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 { ok, badRequest, methodNotAllowed, unauthorized } from 'next-basics';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
import { useCors } from 'lib/middleware';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
|
||||
const unitTypes = ['year', 'month', 'hour', 'day'];
|
||||
|
||||
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))) {
|
||||
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)) {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
const events = await getEventMetrics(websiteId, startDate, endDate, tz, unit, {
|
||||
url,
|
||||
event_name,
|
||||
eventName: event_name,
|
||||
});
|
||||
|
||||
return ok(res, events);
|
69
pages/api/websites/[id]/index.js
Normal file
69
pages/api/websites/[id]/index.js
Normal file
@ -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 { useCors } from 'lib/middleware';
|
||||
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 pageviewColumns = ['url', 'referrer', 'query'];
|
||||
@ -34,25 +34,41 @@ function getColumn(type) {
|
||||
}
|
||||
|
||||
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))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const { id, type, start_at, end_at, url, referrer, os, browser, device, country } = req.query;
|
||||
const {
|
||||
id: websiteId,
|
||||
type,
|
||||
start_at,
|
||||
end_at,
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
} = req.query;
|
||||
|
||||
const websiteId = +id;
|
||||
const startDate = new Date(+start_at);
|
||||
const endDate = new Date(+end_at);
|
||||
|
||||
if (sessionColumns.includes(type)) {
|
||||
let data = await getSessionMetrics(websiteId, startDate, endDate, type, {
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
let data = await getSessionMetrics(websiteId, {
|
||||
startDate,
|
||||
endDate,
|
||||
field: type,
|
||||
filters: {
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
},
|
||||
});
|
||||
|
||||
if (type === 'language') {
|
||||
@ -78,7 +94,7 @@ export default async (req, res) => {
|
||||
let domain;
|
||||
|
||||
if (type === 'referrer') {
|
||||
const website = await getWebsiteById(websiteId);
|
||||
const website = await getWebsiteByUuid(websiteId);
|
||||
|
||||
if (!website) {
|
||||
return badRequest(res);
|
||||
@ -101,7 +117,13 @@ export default async (req, res) => {
|
||||
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);
|
||||
}
|
75
pages/api/websites/[id]/pageviews.js
Normal file
75
pages/api/websites/[id]/pageviews.js
Normal file
@ -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 { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.query;
|
||||
const websiteId = +id;
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
const { id: websiteId } = req.query;
|
||||
|
||||
if (req.method === 'POST') {
|
||||
if (!(await allowQuery(req))) {
|
@ -1,19 +1,29 @@
|
||||
import { getWebsiteStats } from 'queries';
|
||||
import { methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
import { useCors } from 'lib/middleware';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
|
||||
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))) {
|
||||
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 endDate = new Date(+end_at);
|
||||
|
||||
@ -21,21 +31,29 @@ export default async (req, res) => {
|
||||
const prevStartDate = new Date(+start_at - distance);
|
||||
const prevEndDate = new Date(+end_at - distance);
|
||||
|
||||
const metrics = await getWebsiteStats(websiteId, startDate, endDate, {
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
const metrics = await getWebsiteStats(websiteId, {
|
||||
start_at: startDate,
|
||||
end_at: endDate,
|
||||
filters: {
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
},
|
||||
});
|
||||
const prevPeriod = await getWebsiteStats(websiteId, prevStartDate, prevEndDate, {
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
const prevPeriod = await getWebsiteStats(websiteId, {
|
||||
start_at: prevStartDate,
|
||||
end_at: prevEndDate,
|
||||
filters: {
|
||||
url,
|
||||
referrer,
|
||||
os,
|
||||
browser,
|
||||
device,
|
||||
country,
|
||||
},
|
||||
});
|
||||
|
||||
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 { ok, methodNotAllowed, unauthorized } from 'next-basics';
|
||||
import { uuid } from 'lib/crypto';
|
||||
|
||||
export default async (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 userId = +user_id;
|
||||
let account;
|
||||
|
||||
if (accountUuid) {
|
||||
account = await getAccount({ accountUuid: accountUuid });
|
||||
}
|
||||
|
||||
const userId = account ? account.id : +user_id;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (userId && userId !== current_user_id && !is_admin) {
|
||||
if (userId && userId !== currentUserId && !isAdmin) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const websites =
|
||||
is_admin && include_all
|
||||
isAdmin && include_all
|
||||
? await getAllWebsites()
|
||||
: await getUserWebsites(userId || current_user_id);
|
||||
: await getUserWebsites(userId || currentUserId);
|
||||
|
||||
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);
|
||||
};
|
||||
|
@ -8,7 +8,7 @@ export default function ConsolePage({ enabled }) {
|
||||
const { loading } = useRequireLogin();
|
||||
const { user } = useUser();
|
||||
|
||||
if (loading || !enabled || !user?.is_admin) {
|
||||
if (loading || !enabled || !user?.isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -16,6 +16,6 @@ export default function LoginPage({ loginDisabled }) {
|
||||
|
||||
export async function getServerSideProps() {
|
||||
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() {
|
||||
const { loading } = useRequireLogin();
|
||||
|
||||
if (loading) {
|
||||
if (process.env.isCloudMode || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -14,11 +14,11 @@ export default function SharePage() {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { websiteId } = shareToken;
|
||||
const { websiteUuid } = shareToken;
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<WebsiteDetails websiteId={websiteId} />
|
||||
<WebsiteDetails websiteId={websiteUuid} />
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,18 @@
|
||||
"value": "Add account"
|
||||
}
|
||||
],
|
||||
"label.add-column": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add column"
|
||||
}
|
||||
],
|
||||
"label.add-filter": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Add filter"
|
||||
}
|
||||
],
|
||||
"label.add-website": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -155,6 +167,18 @@
|
||||
"value": "Enable share URL"
|
||||
}
|
||||
],
|
||||
"label.event-data": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Event Data"
|
||||
}
|
||||
],
|
||||
"label.field-name": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Field Name"
|
||||
}
|
||||
],
|
||||
"label.invalid": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -313,6 +337,12 @@
|
||||
"value": "Save"
|
||||
}
|
||||
],
|
||||
"label.search": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Search"
|
||||
}
|
||||
],
|
||||
"label.settings": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -373,6 +403,12 @@
|
||||
"value": "Tracking code"
|
||||
}
|
||||
],
|
||||
"label.type": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Type"
|
||||
}
|
||||
],
|
||||
"label.unknown": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -385,6 +421,12 @@
|
||||
"value": "Username"
|
||||
}
|
||||
],
|
||||
"label.value": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Value"
|
||||
}
|
||||
],
|
||||
"label.view-details": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -244,7 +244,7 @@
|
||||
"label.none": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "None"
|
||||
"value": "Байхгүй"
|
||||
}
|
||||
],
|
||||
"label.owner": [
|
||||
@ -397,6 +397,12 @@
|
||||
"value": "Вебүүд"
|
||||
}
|
||||
],
|
||||
"label.yesterday": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Өчигдөр"
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
{
|
||||
"type": 0,
|
||||
@ -456,7 +462,7 @@
|
||||
"message.confirm-reset": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Are your sure you want to reset "
|
||||
"value": "Та "
|
||||
},
|
||||
{
|
||||
"type": 1,
|
||||
@ -464,7 +470,7 @@
|
||||
},
|
||||
{
|
||||
"type": 0,
|
||||
"value": "'s statistics?"
|
||||
"value": "-н тоон үзүүлэлтүүдийг устгахдаа итгэлтэй байна уу?"
|
||||
}
|
||||
],
|
||||
"message.copied": [
|
||||
@ -482,7 +488,7 @@
|
||||
"message.edit-dashboard": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Edit dashboard"
|
||||
"value": "Хянах самбар засах"
|
||||
}
|
||||
],
|
||||
"message.failure": [
|
||||
@ -770,7 +776,7 @@
|
||||
"metrics.query-parameters": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Query parameters"
|
||||
"value": "Query параметр"
|
||||
}
|
||||
],
|
||||
"metrics.referrers": [
|
||||
@ -782,7 +788,7 @@
|
||||
"metrics.screens": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "Screens"
|
||||
"value": "Дэлгэц"
|
||||
}
|
||||
],
|
||||
"metrics.unique-visitors": [
|
||||
|
@ -397,6 +397,12 @@
|
||||
"value": "网站"
|
||||
}
|
||||
],
|
||||
"label.yesterday": [
|
||||
{
|
||||
"type": 0,
|
||||
"value": "昨天"
|
||||
}
|
||||
],
|
||||
"message.active-users": [
|
||||
{
|
||||
"type": 0,
|
||||
|
@ -1,47 +1,47 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import redis, { DELETED } from 'lib/redis';
|
||||
|
||||
export async function deleteAccount(user_id) {
|
||||
export async function deleteAccount(userId) {
|
||||
const { client } = prisma;
|
||||
|
||||
const websites = await client.website.findMany({
|
||||
where: { user_id },
|
||||
select: { website_uuid: true },
|
||||
where: { userId },
|
||||
select: { websiteUuid: true },
|
||||
});
|
||||
|
||||
let websiteUuids = [];
|
||||
|
||||
if (websites.length > 0) {
|
||||
websiteUuids = websites.map(a => a.website_uuid);
|
||||
websiteUuids = websites.map(a => a.websiteUuid);
|
||||
}
|
||||
|
||||
return client
|
||||
.$transaction([
|
||||
client.pageview.deleteMany({
|
||||
where: { session: { website: { user_id } } },
|
||||
where: { session: { website: { userId } } },
|
||||
}),
|
||||
client.event_data.deleteMany({
|
||||
where: { event: { session: { website: { user_id } } } },
|
||||
client.eventData.deleteMany({
|
||||
where: { event: { session: { website: { userId } } } },
|
||||
}),
|
||||
client.event.deleteMany({
|
||||
where: { session: { website: { user_id } } },
|
||||
where: { session: { website: { userId } } },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { website: { user_id } },
|
||||
where: { website: { userId } },
|
||||
}),
|
||||
client.website.deleteMany({
|
||||
where: { user_id },
|
||||
where: { userId },
|
||||
}),
|
||||
client.account.delete({
|
||||
where: {
|
||||
user_id,
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
])
|
||||
.then(async res => {
|
||||
if (redis.client) {
|
||||
for (let i = 0; i < websiteUuids.length; i++) {
|
||||
await redis.client.set(`website:${websiteUuids[i]}`, DELETED);
|
||||
await redis.set(`website:${websiteUuids[i]}`, DELETED);
|
||||
}
|
||||
}
|
||||
|
||||
|
7
queries/admin/account/getAccount.js
Normal file
7
queries/admin/account/getAccount.js
Normal file
@ -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';
|
||||
|
||||
export async function getAccountById(user_id) {
|
||||
export async function getAccountById(userId) {
|
||||
return prisma.client.account.findUnique({
|
||||
where: {
|
||||
user_id,
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -3,17 +3,17 @@ import prisma from 'lib/prisma';
|
||||
export async function getAccounts() {
|
||||
return prisma.client.account.findMany({
|
||||
orderBy: [
|
||||
{ is_admin: 'desc' },
|
||||
{ isAdmin: 'desc' },
|
||||
{
|
||||
username: 'asc',
|
||||
},
|
||||
],
|
||||
select: {
|
||||
user_id: true,
|
||||
id: true,
|
||||
username: true,
|
||||
is_admin: true,
|
||||
created_at: true,
|
||||
updated_at: true,
|
||||
isAdmin: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function updateAccount(user_id, data) {
|
||||
export async function updateAccount(data, where) {
|
||||
return prisma.client.account.update({
|
||||
where: {
|
||||
user_id,
|
||||
},
|
||||
where,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
@ -1,13 +1,13 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import redis from 'lib/redis';
|
||||
|
||||
export async function createWebsite(user_id, data) {
|
||||
export async function createWebsite(userId, data) {
|
||||
return prisma.client.website
|
||||
.create({
|
||||
data: {
|
||||
account: {
|
||||
connect: {
|
||||
user_id,
|
||||
id: userId,
|
||||
},
|
||||
},
|
||||
...data,
|
||||
@ -15,7 +15,7 @@ export async function createWebsite(user_id, data) {
|
||||
})
|
||||
.then(async 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;
|
||||
|
@ -1,31 +1,31 @@
|
||||
import prisma from 'lib/prisma';
|
||||
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 { website_uuid } = await getWebsiteById(website_id);
|
||||
const { websiteUuid } = await getWebsiteByUuid(websiteId);
|
||||
|
||||
return transaction([
|
||||
client.pageview.deleteMany({
|
||||
where: { session: { website: { website_id } } },
|
||||
where: { session: { website: { websiteUuid: websiteId } } },
|
||||
}),
|
||||
client.event_data.deleteMany({
|
||||
where: { event: { session: { website: { website_id } } } },
|
||||
client.eventData.deleteMany({
|
||||
where: { event: { session: { website: { websiteUuid: websiteId } } } },
|
||||
}),
|
||||
client.event.deleteMany({
|
||||
where: { session: { website: { website_id } } },
|
||||
where: { session: { website: { websiteUuid: websiteId } } },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { website: { website_id } },
|
||||
where: { website: { websiteUuid: websiteId } },
|
||||
}),
|
||||
client.website.delete({
|
||||
where: { website_id },
|
||||
where: { websiteUuid: websiteId },
|
||||
}),
|
||||
]).then(async res => {
|
||||
if (redis.client) {
|
||||
await redis.client.set(`website:${website_uuid}`, DELETED);
|
||||
await redis.client.set(`website:${websiteUuid}`, DELETED);
|
||||
}
|
||||
|
||||
return res;
|
||||
|
@ -4,7 +4,7 @@ export async function getAllWebsites() {
|
||||
let data = await prisma.client.website.findMany({
|
||||
orderBy: [
|
||||
{
|
||||
user_id: 'asc',
|
||||
userId: 'asc',
|
||||
},
|
||||
{
|
||||
name: 'asc',
|
||||
|
@ -1,9 +1,9 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getUserWebsites(user_id) {
|
||||
export async function getUserWebsites(userId) {
|
||||
return prisma.client.website.findMany({
|
||||
where: {
|
||||
user_id,
|
||||
userId,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
|
7
queries/admin/website/getWebsite.js
Normal file
7
queries/admin/website/getWebsite.js
Normal file
@ -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';
|
||||
|
||||
export async function getWebsiteById(website_id) {
|
||||
export async function getWebsiteById(websiteId) {
|
||||
return prisma.client.website.findUnique({
|
||||
where: {
|
||||
website_id,
|
||||
id: websiteId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1,9 +1,9 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getWebsiteByShareId(share_id) {
|
||||
export async function getWebsiteByShareId(shareId) {
|
||||
return prisma.client.website.findUnique({
|
||||
where: {
|
||||
share_id,
|
||||
shareId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -1,16 +1,16 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import redis from 'lib/redis';
|
||||
|
||||
export async function getWebsiteByUuid(website_uuid) {
|
||||
export async function getWebsiteByUuid(websiteUuid) {
|
||||
return prisma.client.website
|
||||
.findUnique({
|
||||
where: {
|
||||
website_uuid,
|
||||
websiteUuid,
|
||||
},
|
||||
})
|
||||
.then(async 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;
|
||||
|
@ -1,20 +1,20 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function resetWebsite(website_id) {
|
||||
export async function resetWebsite(websiteId) {
|
||||
const { client, transaction } = prisma;
|
||||
|
||||
return transaction([
|
||||
client.pageview.deleteMany({
|
||||
where: { session: { website: { website_id } } },
|
||||
where: { session: { website: { websiteUuid: websiteId } } },
|
||||
}),
|
||||
client.event_data.deleteMany({
|
||||
where: { event: { session: { website: { website_id } } } },
|
||||
client.eventData.deleteMany({
|
||||
where: { event: { session: { website: { websiteUuid: websiteId } } } },
|
||||
}),
|
||||
client.event.deleteMany({
|
||||
where: { session: { website: { website_id } } },
|
||||
where: { session: { website: { websiteUuid: websiteId } } },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { website: { website_id } },
|
||||
where: { website: { websiteUuid: websiteId } },
|
||||
}),
|
||||
]);
|
||||
}
|
||||
|
@ -1,10 +1,8 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function updateWebsite(website_id, data) {
|
||||
export async function updateWebsite(data, where) {
|
||||
return prisma.client.website.update({
|
||||
where: {
|
||||
website_id,
|
||||
},
|
||||
where,
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
63
queries/analytics/event/getEventData.js
Normal file
63
queries/analytics/event/getEventData.js
Normal file
@ -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
Block a user