mirror of
https://github.com/kremalicious/umami.git
synced 2025-01-11 13:44:01 +01:00
Merge branch 'dev' into analytics
This commit is contained in:
commit
7271951871
@ -36,6 +36,7 @@ DATABASE_URL=connection-url
|
||||
```
|
||||
|
||||
The connection url is in the following format:
|
||||
|
||||
```
|
||||
postgresql://username:mypassword@localhost:5432/mydb
|
||||
|
||||
@ -48,7 +49,7 @@ mysql://username:mypassword@localhost:3306/mydb
|
||||
yarn build
|
||||
```
|
||||
|
||||
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**.
|
||||
The build step will also create tables in your database if you ae installing for the first time. It will also create a login user with username **admin** and password **umami**.
|
||||
|
||||
### Start the application
|
||||
|
||||
@ -69,11 +70,13 @@ docker compose up
|
||||
```
|
||||
|
||||
Alternatively, to pull just the Umami Docker image with PostgreSQL support:
|
||||
|
||||
```bash
|
||||
docker pull docker.umami.dev/umami-software/umami:postgresql-latest
|
||||
```
|
||||
|
||||
Or with MySQL support:
|
||||
|
||||
```bash
|
||||
docker pull docker.umami.dev/umami-software/umami:mysql-latest
|
||||
```
|
||||
|
@ -43,7 +43,7 @@ export default function ChangePasswordForm({ values, onSave, onClose }) {
|
||||
const { user } = useUser();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { ok, error } = await post(`/accounts/${user.accountUuid}/password`, values);
|
||||
const { ok, error } = await post(`/users/${user.id}/password`, values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
|
@ -42,9 +42,11 @@ export default function LoginForm() {
|
||||
});
|
||||
|
||||
if (ok) {
|
||||
setItem(AUTH_TOKEN, data.token);
|
||||
const { user, token } = data;
|
||||
|
||||
setUser(data.user);
|
||||
setItem(AUTH_TOKEN, token);
|
||||
|
||||
setUser(user);
|
||||
|
||||
await router.push('/');
|
||||
|
||||
|
@ -26,7 +26,7 @@ export default function TrackingCodeForm({ values, onClose }) {
|
||||
rows={3}
|
||||
cols={60}
|
||||
spellCheck={false}
|
||||
defaultValue={`<script async defer data-website-id="${values.websiteUuid}" src="${
|
||||
defaultValue={`<script async defer data-website-id="${values.id}" src="${
|
||||
document.location.origin
|
||||
}${basePath}/${trackerScriptName ? `${trackerScriptName}.js` : 'umami.js'}"></script>`}
|
||||
readOnly
|
||||
|
@ -28,13 +28,13 @@ const validate = ({ id, username, password }) => {
|
||||
return errors;
|
||||
};
|
||||
|
||||
export default function AccountEditForm({ values, onSave, onClose }) {
|
||||
export default function UserEditForm({ values, onSave, onClose }) {
|
||||
const { post } = useApi();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { id } = values;
|
||||
const { ok, data } = await post(id ? `/accounts/${id}` : '/accounts', values);
|
||||
const { ok, data } = await post(id ? `/users/${id}` : '/users', values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
@ -37,7 +37,7 @@ const validate = ({ name, domain }) => {
|
||||
return errors;
|
||||
};
|
||||
|
||||
const OwnerDropDown = ({ user, accounts }) => {
|
||||
const OwnerDropDown = ({ user, users }) => {
|
||||
const { setFieldValue, values } = useFormikContext();
|
||||
|
||||
useEffect(() => {
|
||||
@ -46,7 +46,7 @@ const OwnerDropDown = ({ user, accounts }) => {
|
||||
} else if (user?.id && values.owner === '') {
|
||||
setFieldValue('owner', user.id.toString());
|
||||
}
|
||||
}, [accounts, setFieldValue, user, values]);
|
||||
}, [users, setFieldValue, user, values]);
|
||||
|
||||
if (user?.isAdmin) {
|
||||
return (
|
||||
@ -56,7 +56,7 @@ const OwnerDropDown = ({ user, accounts }) => {
|
||||
</label>
|
||||
<div>
|
||||
<Field as="select" name="owner" className={styles.dropdown}>
|
||||
{accounts?.map(acc => (
|
||||
{users?.map(acc => (
|
||||
<option key={acc.id} value={acc.id}>
|
||||
{acc.username}
|
||||
</option>
|
||||
@ -73,14 +73,14 @@ const OwnerDropDown = ({ user, accounts }) => {
|
||||
|
||||
export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
const { post } = useApi();
|
||||
const { data: accounts } = useFetch(`/accounts`);
|
||||
const { data: users } = useFetch(`/users`);
|
||||
const { user } = useUser();
|
||||
const [message, setMessage] = useState();
|
||||
|
||||
const handleSubmit = async values => {
|
||||
const { websiteUuid: websiteId } = values;
|
||||
const { id } = values;
|
||||
|
||||
const { ok, data } = await post(websiteId ? `/websites/${websiteId}` : '/websites', values);
|
||||
const { ok, data } = await post(id ? `/websites/${id}` : '/websites', values);
|
||||
|
||||
if (ok) {
|
||||
onSave();
|
||||
@ -125,7 +125,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
|
||||
<FormError name="domain" />
|
||||
</div>
|
||||
</FormRow>
|
||||
<OwnerDropDown accounts={accounts} user={user} />
|
||||
<OwnerDropDown users={users} user={user} />
|
||||
<FormRow>
|
||||
<label />
|
||||
<Field name="enableShareUrl">
|
||||
|
@ -13,11 +13,12 @@ import useConfig from 'hooks/useConfig';
|
||||
import useUser from 'hooks/useUser';
|
||||
import Logo from 'assets/logo.svg';
|
||||
import styles from './Header.module.css';
|
||||
import SettingsButton from '../settings/SettingsButton';
|
||||
|
||||
export default function Header() {
|
||||
const { user } = useUser();
|
||||
const { pathname } = useRouter();
|
||||
const { updatesDisabled } = useConfig();
|
||||
const { updatesDisabled, adminDisabled } = useConfig();
|
||||
const isSharePage = pathname.includes('/share/');
|
||||
const allowUpdate = user?.isAdmin && !updatesDisabled && !isSharePage;
|
||||
|
||||
@ -30,7 +31,7 @@ export default function Header() {
|
||||
<Link href={isSharePage ? HOMEPAGE_URL : '/'}>umami</Link>
|
||||
</div>
|
||||
<HamburgerButton />
|
||||
{user && (
|
||||
{user && !adminDisabled && (
|
||||
<div className={styles.links}>
|
||||
<Link href="/dashboard">
|
||||
<FormattedMessage id="label.dashboard" defaultMessage="Dashboard" />
|
||||
@ -38,17 +39,16 @@ export default function Header() {
|
||||
<Link href="/realtime">
|
||||
<FormattedMessage id="label.realtime" defaultMessage="Realtime" />
|
||||
</Link>
|
||||
{!process.env.isCloudMode && (
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
)}
|
||||
<Link href="/settings">
|
||||
<FormattedMessage id="label.settings" defaultMessage="Settings" />
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.buttons}>
|
||||
<ThemeButton />
|
||||
<LanguageButton menuAlign="right" />
|
||||
{user && <UserButton />}
|
||||
<SettingsButton />
|
||||
{user && !adminDisabled && <UserButton />}
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
|
@ -17,9 +17,9 @@ export default function MenuLayout({
|
||||
|
||||
function handleSelect(url) {
|
||||
if (replace) {
|
||||
router.replace(url);
|
||||
router.replace(url, undefined, { shallow: true });
|
||||
} else {
|
||||
router.push(url);
|
||||
router.push(url, undefined, { shallow: true });
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useSpring, animated } from 'react-spring';
|
||||
import { formatNumber } from '../../lib/format';
|
||||
import { formatNumber } from 'lib/format';
|
||||
import styles from './MetricCard.module.css';
|
||||
|
||||
const MetricCard = ({
|
||||
|
@ -14,9 +14,9 @@ export default function RealtimeHeader({ websites, data, websiteId, onSelect })
|
||||
value: null,
|
||||
},
|
||||
].concat(
|
||||
websites.map(({ name, websiteUuid }, index) => ({
|
||||
websites.map(({ name, id }, index) => ({
|
||||
label: name,
|
||||
value: websiteUuid,
|
||||
value: id,
|
||||
divider: index === 0,
|
||||
})),
|
||||
);
|
||||
|
@ -24,7 +24,7 @@ export default function DashboardEdit({ websites }) {
|
||||
const ordered = useMemo(
|
||||
() =>
|
||||
websites
|
||||
.map(website => ({ ...website, order: order.indexOf(website.websiteUuid) }))
|
||||
.map(website => ({ ...website, order: order.indexOf(website.id) }))
|
||||
.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?.websiteUuid || 0));
|
||||
setOrder(orderedWebsites.map(website => website?.id || 0));
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
@ -76,12 +76,8 @@ export default function DashboardEdit({ websites }) {
|
||||
ref={provided.innerRef}
|
||||
style={{ marginBottom: snapshot.isDraggingOver ? 260 : null }}
|
||||
>
|
||||
{ordered.map(({ websiteUuid, name, domain }, index) => (
|
||||
<Draggable
|
||||
key={websiteUuid}
|
||||
draggableId={`${dragId}-${websiteUuid}`}
|
||||
index={index}
|
||||
>
|
||||
{ordered.map(({ id, name, domain }, index) => (
|
||||
<Draggable key={id} draggableId={`${dragId}-${id}`} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
|
@ -32,7 +32,7 @@ export default function RealtimeDashboard() {
|
||||
const { locale } = useLocale();
|
||||
const countryNames = useCountryNames(locale);
|
||||
const [data, setData] = useState();
|
||||
const [websiteUuid, setWebsiteUuid] = useState(null);
|
||||
const [websiteId, setWebsiteId] = useState(null);
|
||||
const { data: init, loading } = useFetch('/realtime/init');
|
||||
const { data: updates } = useFetch('/realtime/update', {
|
||||
params: { start_at: data?.timestamp },
|
||||
@ -50,8 +50,8 @@ export default function RealtimeDashboard() {
|
||||
if (data) {
|
||||
const { pageviews, sessions, events } = data;
|
||||
|
||||
if (websiteUuid) {
|
||||
const { id } = init.websites.find(n => n.websiteUuid === websiteUuid);
|
||||
if (websiteId) {
|
||||
const { id } = init.websites.find(n => n.id === websiteId);
|
||||
return {
|
||||
pageviews: filterWebsite(pageviews, id),
|
||||
sessions: filterWebsite(sessions, id),
|
||||
@ -61,7 +61,7 @@ export default function RealtimeDashboard() {
|
||||
}
|
||||
|
||||
return data;
|
||||
}, [data, websiteUuid]);
|
||||
}, [data, websiteId]);
|
||||
|
||||
const countries = useMemo(() => {
|
||||
if (realtimeData?.sessions) {
|
||||
@ -118,9 +118,9 @@ export default function RealtimeDashboard() {
|
||||
<Page>
|
||||
<RealtimeHeader
|
||||
websites={websites}
|
||||
websiteId={websiteUuid}
|
||||
websiteId={websiteId}
|
||||
data={{ ...realtimeData, countries }}
|
||||
onSelect={setWebsiteUuid}
|
||||
onSelect={setWebsiteId}
|
||||
/>
|
||||
<div className={styles.chart}>
|
||||
<RealtimeChart data={realtimeData} unit="minute" records={REALTIME_RANGE} />
|
||||
@ -128,10 +128,10 @@ export default function RealtimeDashboard() {
|
||||
<GridLayout>
|
||||
<GridRow>
|
||||
<GridColumn xs={12} lg={4}>
|
||||
<RealtimeViews websiteId={websiteUuid} data={realtimeData} websites={websites} />
|
||||
<RealtimeViews websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
<GridColumn xs={12} lg={8}>
|
||||
<RealtimeLog websiteId={websiteUuid} data={realtimeData} websites={websites} />
|
||||
<RealtimeLog websiteId={websiteId} data={realtimeData} websites={websites} />
|
||||
</GridColumn>
|
||||
</GridRow>
|
||||
<GridRow>
|
||||
|
@ -4,12 +4,12 @@ import { useRouter } from 'next/router';
|
||||
import Page from 'components/layout/Page';
|
||||
import MenuLayout from 'components/layout/MenuLayout';
|
||||
import WebsiteSettings from 'components/settings/WebsiteSettings';
|
||||
import AccountSettings from 'components/settings/AccountSettings';
|
||||
import UserSettings from 'components/settings/UserSettings';
|
||||
import ProfileSettings from 'components/settings/ProfileSettings';
|
||||
import useUser from 'hooks/useUser';
|
||||
|
||||
const WEBSITES = '/settings';
|
||||
const ACCOUNTS = '/settings/accounts';
|
||||
const ACCOUNTS = '/settings/users';
|
||||
const PROFILE = '/settings/profile';
|
||||
|
||||
export default function Settings() {
|
||||
@ -28,7 +28,7 @@ export default function Settings() {
|
||||
value: WEBSITES,
|
||||
},
|
||||
{
|
||||
label: <FormattedMessage id="label.accounts" defaultMessage="Accounts" />,
|
||||
label: <FormattedMessage id="label.users" defaultMessage="Users" />,
|
||||
value: ACCOUNTS,
|
||||
hidden: !user?.isAdmin,
|
||||
},
|
||||
@ -42,7 +42,7 @@ export default function Settings() {
|
||||
<Page>
|
||||
<MenuLayout menu={menuOptions} selectedOption={option} onMenuSelect={setOption}>
|
||||
{pathname === WEBSITES && <WebsiteSettings />}
|
||||
{pathname === ACCOUNTS && <AccountSettings />}
|
||||
{pathname === ACCOUNTS && <UserSettings />}
|
||||
{pathname === PROFILE && <ProfileSettings />}
|
||||
</MenuLayout>
|
||||
</Page>
|
||||
|
@ -12,7 +12,7 @@ import useFetch from 'hooks/useFetch';
|
||||
import styles from './TestConsole.module.css';
|
||||
|
||||
export default function TestConsole() {
|
||||
const { data } = useFetch('/websites');
|
||||
const { data } = useFetch('/websites?include_all=true');
|
||||
const router = useRouter();
|
||||
const {
|
||||
basePath,
|
||||
@ -24,9 +24,9 @@ export default function TestConsole() {
|
||||
return null;
|
||||
}
|
||||
|
||||
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;
|
||||
const options = data.map(({ name, id }) => ({ label: name, value: id }));
|
||||
const website = data.find(({ id }) => websiteId === id);
|
||||
const selectedValue = options.find(({ value }) => value === website?.id)?.value;
|
||||
|
||||
function handleSelect(value) {
|
||||
router.push(`/console/${value}`);
|
||||
@ -46,7 +46,7 @@ export default function TestConsole() {
|
||||
<script
|
||||
async
|
||||
defer
|
||||
data-website-id={website.websiteUuid}
|
||||
data-website-id={website.id}
|
||||
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.websiteUuid}
|
||||
websiteId={website.id}
|
||||
title={website.name}
|
||||
domain={website.domain}
|
||||
showLink
|
||||
/>
|
||||
<PageHeader>Events</PageHeader>
|
||||
<EventsChart websiteId={website.websiteUuid} />
|
||||
<EventsChart websiteId={website.id} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
@ -27,7 +27,7 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||
const ordered = useMemo(
|
||||
() =>
|
||||
websites
|
||||
.map(website => ({ ...website, order: websiteOrder.indexOf(website.websiteUuid) || 0 }))
|
||||
.map(website => ({ ...website, order: websiteOrder.indexOf(website.id) || 0 }))
|
||||
.sort(firstBy('order')),
|
||||
[websites, websiteOrder],
|
||||
);
|
||||
@ -46,11 +46,11 @@ export default function WebsiteList({ websites, showCharts, limit }) {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{ordered.map(({ websiteUuid, name, domain }, index) =>
|
||||
{ordered.map(({ id, name, domain }, index) =>
|
||||
index < limit ? (
|
||||
<div key={websiteUuid} className={styles.website}>
|
||||
<div key={id} className={styles.website}>
|
||||
<WebsiteChart
|
||||
websiteId={websiteUuid}
|
||||
websiteId={id}
|
||||
title={name}
|
||||
domain={domain}
|
||||
showChart={showCharts}
|
||||
|
@ -5,7 +5,7 @@ import Button from 'components/common/Button';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import { DEFAULT_LOCALE } from 'lib/constants';
|
||||
import styles from './TimezoneSetting.module.css';
|
||||
import { languages } from '../../lib/lang';
|
||||
import { languages } from 'lib/lang';
|
||||
|
||||
export default function LanguageSetting() {
|
||||
const { locale, saveLocale } = useLocale();
|
||||
|
45
components/settings/SettingsButton.js
Normal file
45
components/settings/SettingsButton.js
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { useRef, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import TimezoneSetting from './TimezoneSetting';
|
||||
import DateRangeSetting from './DateRangeSetting';
|
||||
import Button from 'components/common/Button';
|
||||
import styles from './SettingsButton.module.css';
|
||||
import Gear from 'assets/gear.svg';
|
||||
import useDocumentClick from '../../hooks/useDocumentClick';
|
||||
|
||||
export default function SettingsButton() {
|
||||
const [show, setShow] = useState(false);
|
||||
const ref = useRef();
|
||||
|
||||
function handleClick() {
|
||||
setShow(state => !state);
|
||||
}
|
||||
|
||||
useDocumentClick(e => {
|
||||
if (!ref.current?.contains(e.target)) {
|
||||
setShow(false);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={styles.button} ref={ref}>
|
||||
<Button icon={<Gear />} variant="light" onClick={handleClick} />
|
||||
{show && (
|
||||
<div className={styles.panel}>
|
||||
<dt>
|
||||
<FormattedMessage id="label.timezone" defaultMessage="Timezone" />
|
||||
</dt>
|
||||
<dd>
|
||||
<TimezoneSetting />
|
||||
</dd>
|
||||
<dt>
|
||||
<FormattedMessage id="label.default-date-range" defaultMessage="Default date range" />
|
||||
</dt>
|
||||
<dd>
|
||||
<DateRangeSetting />
|
||||
</dd>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
20
components/settings/SettingsButton.module.css
Normal file
20
components/settings/SettingsButton.module.css
Normal file
@ -0,0 +1,20 @@
|
||||
.button {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.panel {
|
||||
background: var(--gray50);
|
||||
border: 1px solid var(--gray500);
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
right: 0;
|
||||
padding: 20px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.panel dd {
|
||||
display: flex;
|
||||
}
|
@ -8,10 +8,12 @@ import User from 'assets/user.svg';
|
||||
import styles from './UserButton.module.css';
|
||||
import { AUTH_TOKEN } from 'lib/constants';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
export default function UserButton() {
|
||||
const { user } = useUser();
|
||||
const router = useRouter();
|
||||
const { adminDisabled } = useConfig();
|
||||
|
||||
const menuOptions = [
|
||||
{
|
||||
@ -28,7 +30,7 @@ export default function UserButton() {
|
||||
{
|
||||
label: <FormattedMessage id="label.profile" defaultMessage="Profile" />,
|
||||
value: 'profile',
|
||||
hidden: process.env.isCloudMode,
|
||||
hidden: adminDisabled,
|
||||
},
|
||||
{ label: <FormattedMessage id="label.logout" defaultMessage="Logout" />, value: 'logout' },
|
||||
];
|
||||
|
@ -8,7 +8,7 @@ import Icon from 'components/common/Icon';
|
||||
import Table from 'components/common/Table';
|
||||
import Modal from 'components/common/Modal';
|
||||
import Toast from 'components/common/Toast';
|
||||
import AccountEditForm from 'components/forms/AccountEditForm';
|
||||
import UserEditForm from 'components/forms/UserEditForm';
|
||||
import ButtonLayout from 'components/layout/ButtonLayout';
|
||||
import DeleteForm from 'components/forms/DeleteForm';
|
||||
import useFetch from 'hooks/useFetch';
|
||||
@ -17,21 +17,21 @@ import Plus from 'assets/plus.svg';
|
||||
import Trash from 'assets/trash.svg';
|
||||
import Check from 'assets/check.svg';
|
||||
import LinkIcon from 'assets/external-link.svg';
|
||||
import styles from './AccountSettings.module.css';
|
||||
import styles from './UserSettings.module.css';
|
||||
|
||||
export default function AccountSettings() {
|
||||
const [addAccount, setAddAccount] = useState();
|
||||
const [editAccount, setEditAccount] = useState();
|
||||
const [deleteAccount, setDeleteAccount] = useState();
|
||||
export default function UserSettings() {
|
||||
const [addUser, setAddUser] = useState();
|
||||
const [editUser, setEditUser] = useState();
|
||||
const [deleteUser, setDeleteUser] = useState();
|
||||
const [saved, setSaved] = useState(0);
|
||||
const [message, setMessage] = useState();
|
||||
const { data } = useFetch(`/accounts`, {}, [saved]);
|
||||
const { data } = useFetch(`/users`, {}, [saved]);
|
||||
|
||||
const Checkmark = ({ isAdmin }) => (isAdmin ? <Icon icon={<Check />} size="medium" /> : null);
|
||||
|
||||
const DashboardLink = row => {
|
||||
return (
|
||||
<Link href={`/dashboard/${row.accountUuid}/${row.username}`}>
|
||||
<Link href={`/dashboard/${row.id}/${row.username}`}>
|
||||
<a>
|
||||
<Icon icon={<LinkIcon />} />
|
||||
</a>
|
||||
@ -41,11 +41,11 @@ export default function AccountSettings() {
|
||||
|
||||
const Buttons = row => (
|
||||
<ButtonLayout align="right">
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditAccount(row)}>
|
||||
<Button icon={<Pen />} size="small" onClick={() => setEditUser(row)}>
|
||||
<FormattedMessage id="label.edit" defaultMessage="Edit" />
|
||||
</Button>
|
||||
{!row.isAdmin && (
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteAccount(row)}>
|
||||
<Button icon={<Trash />} size="small" onClick={() => setDeleteUser(row)}>
|
||||
<FormattedMessage id="label.delete" defaultMessage="Delete" />
|
||||
</Button>
|
||||
)}
|
||||
@ -84,9 +84,9 @@ export default function AccountSettings() {
|
||||
}
|
||||
|
||||
function handleClose() {
|
||||
setEditAccount(null);
|
||||
setAddAccount(null);
|
||||
setDeleteAccount(null);
|
||||
setEditUser(null);
|
||||
setAddUser(null);
|
||||
setDeleteUser(null);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
@ -97,33 +97,31 @@ export default function AccountSettings() {
|
||||
<>
|
||||
<PageHeader>
|
||||
<div>
|
||||
<FormattedMessage id="label.accounts" defaultMessage="Accounts" />
|
||||
<FormattedMessage id="label.users" defaultMessage="Users" />
|
||||
</div>
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddAccount(true)}>
|
||||
<FormattedMessage id="label.add-account" defaultMessage="Add account" />
|
||||
<Button icon={<Plus />} size="small" onClick={() => setAddUser(true)}>
|
||||
<FormattedMessage id="label.add-user" defaultMessage="Add user" />
|
||||
</Button>
|
||||
</PageHeader>
|
||||
<Table columns={columns} rows={data} />
|
||||
{editAccount && (
|
||||
<Modal title={<FormattedMessage id="label.edit-account" defaultMessage="Edit account" />}>
|
||||
<AccountEditForm
|
||||
values={{ ...editAccount, password: '' }}
|
||||
{editUser && (
|
||||
<Modal title={<FormattedMessage id="label.edit-user" defaultMessage="Edit user" />}>
|
||||
<UserEditForm
|
||||
values={{ ...editUser, password: '' }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{addAccount && (
|
||||
<Modal title={<FormattedMessage id="label.add-account" defaultMessage="Add account" />}>
|
||||
<AccountEditForm onSave={handleSave} onClose={handleClose} />
|
||||
{addUser && (
|
||||
<Modal title={<FormattedMessage id="label.add-user" defaultMessage="Add user" />}>
|
||||
<UserEditForm onSave={handleSave} onClose={handleClose} />
|
||||
</Modal>
|
||||
)}
|
||||
{deleteAccount && (
|
||||
<Modal
|
||||
title={<FormattedMessage id="label.delete-account" defaultMessage="Delete account" />}
|
||||
>
|
||||
{deleteUser && (
|
||||
<Modal title={<FormattedMessage id="label.delete-user" defaultMessage="Delete user" />}>
|
||||
<DeleteForm
|
||||
values={{ type: 'accounts', id: deleteAccount.id, name: deleteAccount.username }}
|
||||
values={{ type: 'users', id: deleteUser.id, name: deleteUser.username }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
@ -46,7 +46,7 @@ export default function WebsiteSettings() {
|
||||
icon={<LinkIcon />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="message.get-share-url" defaultMessage="Get share URL" />}
|
||||
tooltipId={`button-share-${row.websiteUuid}`}
|
||||
tooltipId={`button-share-${row.id}`}
|
||||
onClick={() => setShowUrl(row)}
|
||||
/>
|
||||
)}
|
||||
@ -56,46 +56,42 @@ export default function WebsiteSettings() {
|
||||
tooltip={
|
||||
<FormattedMessage id="message.get-tracking-code" defaultMessage="Get tracking code" />
|
||||
}
|
||||
tooltipId={`button-code-${row.websiteUuid}`}
|
||||
tooltipId={`button-code-${row.id}`}
|
||||
onClick={() => setShowCode(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Pen />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.edit" defaultMessage="Edit" />}
|
||||
tooltipId={`button-edit-${row.websiteUuid}`}
|
||||
tooltipId={`button-edit-${row.id}`}
|
||||
onClick={() => setEditWebsite(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Reset />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.reset" defaultMessage="Reset" />}
|
||||
tooltipId={`button-reset-${row.websiteUuid}`}
|
||||
tooltipId={`button-reset-${row.id}`}
|
||||
onClick={() => setResetWebsite(row)}
|
||||
/>
|
||||
<Button
|
||||
icon={<Trash />}
|
||||
size="small"
|
||||
tooltip={<FormattedMessage id="label.delete" defaultMessage="Delete" />}
|
||||
tooltipId={`button-delete-${row.websiteUuid}`}
|
||||
tooltipId={`button-delete-${row.id}`}
|
||||
onClick={() => setDeleteWebsite(row)}
|
||||
/>
|
||||
</ButtonLayout>
|
||||
);
|
||||
|
||||
const DetailsLink = ({ websiteUuid, name, domain }) => (
|
||||
<Link
|
||||
className={styles.detailLink}
|
||||
href="/websites/[...id]"
|
||||
as={`/websites/${websiteUuid}/${name}`}
|
||||
>
|
||||
const DetailsLink = ({ id, name, domain }) => (
|
||||
<Link className={styles.detailLink} href="/websites/[...id]" as={`/websites/${id}/${name}`}>
|
||||
<Favicon domain={domain} />
|
||||
<OverflowText tooltipId={`${websiteUuid}-name`}>{name}</OverflowText>
|
||||
<OverflowText tooltipId={`${id}-name`}>{name}</OverflowText>
|
||||
</Link>
|
||||
);
|
||||
|
||||
const Domain = ({ domain, websiteUuid }) => (
|
||||
<OverflowText tooltipId={`${websiteUuid}-domain`}>{domain}</OverflowText>
|
||||
const Domain = ({ domain, id }) => (
|
||||
<OverflowText tooltipId={`${id}-domain`}>{domain}</OverflowText>
|
||||
);
|
||||
|
||||
const adminColumns = [
|
||||
@ -112,7 +108,7 @@ export default function WebsiteSettings() {
|
||||
render: Domain,
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
key: 'user',
|
||||
label: <FormattedMessage id="label.owner" defaultMessage="Owner" />,
|
||||
className: 'col-12 col-lg-4 col-xl-1',
|
||||
},
|
||||
@ -203,7 +199,7 @@ export default function WebsiteSettings() {
|
||||
title={<FormattedMessage id="label.reset-website" defaultMessage="Reset statistics" />}
|
||||
>
|
||||
<ResetForm
|
||||
values={{ type: 'websites', id: resetWebsite.websiteUuid, name: resetWebsite.name }}
|
||||
values={{ type: 'websites', id: resetWebsite.id, name: resetWebsite.name }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
@ -214,7 +210,7 @@ export default function WebsiteSettings() {
|
||||
title={<FormattedMessage id="label.delete-website" defaultMessage="Delete website" />}
|
||||
>
|
||||
<DeleteForm
|
||||
values={{ type: 'websites', id: deleteWebsite.websiteUuid, name: deleteWebsite.name }}
|
||||
values={{ type: 'websites', id: deleteWebsite.id, name: deleteWebsite.name }}
|
||||
onSave={handleSave}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
|
@ -6,6 +6,7 @@ CREATE TABLE event
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
event_id Nullable(UUID),
|
||||
rev_id UInt32,
|
||||
--session
|
||||
hostname LowCardinality(String),
|
||||
browser LowCardinality(String),
|
||||
@ -30,6 +31,7 @@ CREATE TABLE event_queue (
|
||||
website_id UUID,
|
||||
session_id UUID,
|
||||
event_id Nullable(UUID),
|
||||
rev_id UInt32,
|
||||
url String,
|
||||
referrer String,
|
||||
hostname LowCardinality(String),
|
||||
@ -55,6 +57,7 @@ CREATE MATERIALIZED VIEW event_queue_mv TO event AS
|
||||
SELECT website_id,
|
||||
session_id,
|
||||
event_id,
|
||||
rev_id,
|
||||
url,
|
||||
referrer,
|
||||
hostname,
|
||||
|
@ -1,132 +0,0 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "account" (
|
||||
"user_id" SERIAL NOT NULL,
|
||||
"username" VARCHAR(255) NOT NULL,
|
||||
"password" VARCHAR(60) NOT NULL,
|
||||
"is_admin" BOOLEAN NOT NULL DEFAULT false,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("user_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "event" (
|
||||
"event_id" SERIAL NOT NULL,
|
||||
"website_id" INTEGER NOT NULL,
|
||||
"session_id" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"url" VARCHAR(500) NOT NULL,
|
||||
"event_type" VARCHAR(50) NOT NULL,
|
||||
"event_value" VARCHAR(50) NOT NULL,
|
||||
|
||||
PRIMARY KEY ("event_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "pageview" (
|
||||
"view_id" SERIAL NOT NULL,
|
||||
"website_id" INTEGER NOT NULL,
|
||||
"session_id" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"url" VARCHAR(500) NOT NULL,
|
||||
"referrer" VARCHAR(500),
|
||||
|
||||
PRIMARY KEY ("view_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "session" (
|
||||
"session_id" SERIAL NOT NULL,
|
||||
"session_uuid" UUID NOT NULL,
|
||||
"website_id" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"hostname" VARCHAR(100),
|
||||
"browser" VARCHAR(20),
|
||||
"os" VARCHAR(20),
|
||||
"device" VARCHAR(20),
|
||||
"screen" VARCHAR(11),
|
||||
"language" VARCHAR(35),
|
||||
"country" CHAR(2),
|
||||
|
||||
PRIMARY KEY ("session_id")
|
||||
);
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "website" (
|
||||
"website_id" SERIAL NOT NULL,
|
||||
"website_uuid" UUID NOT NULL,
|
||||
"user_id" INTEGER NOT NULL,
|
||||
"name" VARCHAR(100) NOT NULL,
|
||||
"domain" VARCHAR(500),
|
||||
"share_id" VARCHAR(64),
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
PRIMARY KEY ("website_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "account.username_unique" ON "account"("username");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_created_at_idx" ON "event"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_session_id_idx" ON "event"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_website_id_idx" ON "event"("website_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "session_created_at_idx" ON "session"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "session_website_id_idx" ON "session"("website_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "website_user_id_idx" ON "website"("user_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- CreateAdminUser
|
||||
INSERT INTO account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);
|
@ -1,66 +0,0 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "event" DROP CONSTRAINT "event_session_id_fkey";
|
||||
ALTER TABLE "event" DROP CONSTRAINT "event_website_id_fkey";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX "event_pkey" RENAME TO "event_old_pkey";
|
||||
ALTER INDEX "event_created_at_idx" RENAME TO "event_old_created_at_idx";
|
||||
ALTER INDEX "event_session_id_idx" RENAME TO "event_old_session_id_idx";
|
||||
ALTER INDEX "event_website_id_idx" RENAME TO "event_old_website_id_idx";
|
||||
|
||||
-- RenameTable
|
||||
ALTER TABLE "event" RENAME TO "_event_old";
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "event" (
|
||||
"event_id" SERIAL NOT NULL,
|
||||
"website_id" INTEGER NOT NULL,
|
||||
"session_id" INTEGER NOT NULL,
|
||||
"created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,
|
||||
"url" VARCHAR(500) NOT NULL,
|
||||
"event_name" VARCHAR(50) NOT NULL,
|
||||
|
||||
PRIMARY KEY ("event_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_created_at_idx" ON "event"("created_at");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_session_id_idx" ON "event"("session_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "event_website_id_idx" ON "event"("website_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event" ADD CONSTRAINT "event_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event" ADD CONSTRAINT "event_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
-- CreateTable
|
||||
CREATE TABLE "event_data" (
|
||||
"event_data_id" SERIAL NOT NULL,
|
||||
"event_id" INTEGER NOT NULL,
|
||||
"event_data" JSONB NOT NULL,
|
||||
|
||||
CONSTRAINT "event_data_pkey" PRIMARY KEY ("event_data_id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "event_data_event_id_key" ON "event_data"("event_id");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event_data" ADD CONSTRAINT "event_data_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "event"("event_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX IF EXISTS "account.username_unique" RENAME TO "account_username_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX IF EXISTS "session.session_uuid_unique" RENAME TO "session_session_uuid_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX IF EXISTS "website.share_id_unique" RENAME TO "website_share_id_key";
|
||||
|
||||
-- RenameIndex
|
||||
ALTER INDEX IF EXISTS "website.website_uuid_unique" RENAME TO "website_website_uuid_key";
|
@ -1,35 +0,0 @@
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "event" DROP CONSTRAINT IF EXISTS "event_session_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "event" DROP CONSTRAINT IF EXISTS "event_website_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "pageview" DROP CONSTRAINT IF EXISTS "pageview_session_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "pageview" DROP CONSTRAINT IF EXISTS "pageview_website_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "session" DROP CONSTRAINT IF EXISTS "session_website_id_fkey";
|
||||
|
||||
-- DropForeignKey
|
||||
ALTER TABLE "website" DROP CONSTRAINT IF EXISTS "website_user_id_fkey";
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event" ADD CONSTRAINT "event_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "event" ADD CONSTRAINT "event_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pageview" ADD CONSTRAINT "pageview_session_id_fkey" FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "pageview" ADD CONSTRAINT "pageview_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_website_id_fkey" FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "website" ADD CONSTRAINT "website_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
@ -1,38 +0,0 @@
|
||||
-- CreateExtension
|
||||
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||
|
||||
-- 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");
|
@ -1,3 +0,0 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
provider = "postgresql"
|
@ -7,97 +7,204 @@ datasource db {
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model account {
|
||||
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[]
|
||||
model User {
|
||||
id String @id @unique @map("user_id") @db.Uuid
|
||||
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)
|
||||
|
||||
@@index([accountUuid])
|
||||
groupRole GroupRole[]
|
||||
groupUser GroupUser[]
|
||||
userRole UserRole[]
|
||||
teamWebsite TeamWebsite[]
|
||||
teamUser TeamUser[]
|
||||
userWebsite UserWebsite[]
|
||||
website Website[]
|
||||
|
||||
@@map("user")
|
||||
}
|
||||
|
||||
model event {
|
||||
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?
|
||||
model Session {
|
||||
id String @id @unique @map("session_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
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)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([websiteId])
|
||||
@@index([eventUuid])
|
||||
@@map("session")
|
||||
}
|
||||
|
||||
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])
|
||||
model Website {
|
||||
id String @id @unique @map("website_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
name String @db.VarChar(100)
|
||||
domain String? @db.VarChar(500)
|
||||
shareId String? @unique @map("share_id") @db.VarChar(64)
|
||||
revId Int @default(0) @map("rev_id") @db.Integer
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
@@map("event_data")
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
teamWebsite TeamWebsite[]
|
||||
userWebsite UserWebsite[]
|
||||
|
||||
@@index([userId])
|
||||
@@index([createdAt])
|
||||
@@index([shareId])
|
||||
@@map("website")
|
||||
}
|
||||
|
||||
model pageview {
|
||||
id Int @id @default(autoincrement()) @map("view_id")
|
||||
websiteId Int @map("website_id")
|
||||
sessionId Int @map("session_id")
|
||||
model WebsiteEvent {
|
||||
id String @id() @map("event_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
sessionId String @map("session_id") @db.Uuid
|
||||
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])
|
||||
eventName String @map("event_name") @db.VarChar(50)
|
||||
eventData Json @map("event_data")
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([sessionId])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, createdAt])
|
||||
@@index([websiteId])
|
||||
@@index([websiteId, sessionId, createdAt])
|
||||
@@map("website_event")
|
||||
}
|
||||
|
||||
model session {
|
||||
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[]
|
||||
model Group {
|
||||
id String @id() @unique() @map("group_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
@@index([createdAt])
|
||||
@@index([websiteId])
|
||||
@@index([sessionUuid])
|
||||
groupRoles GroupRole[]
|
||||
groupUsers GroupUser[]
|
||||
|
||||
@@map("group")
|
||||
}
|
||||
|
||||
model website {
|
||||
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[]
|
||||
model GroupRole {
|
||||
id String @id() @unique() @map("group_role_id") @db.Uuid
|
||||
groupId String @map("group_id") @db.Uuid
|
||||
roleId String @map("role_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
@@index([userId])
|
||||
@@index([websiteUuid])
|
||||
group Group @relation(fields: [groupId], references: [id])
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId String? @db.Uuid
|
||||
|
||||
@@map("group_role")
|
||||
}
|
||||
|
||||
model GroupUser {
|
||||
id String @id() @unique() @map("group_user_id") @db.Uuid
|
||||
groupId String @map("group_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
group Group @relation(fields: [groupId], references: [id])
|
||||
User User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("group_user")
|
||||
}
|
||||
|
||||
model Permission {
|
||||
id String @id() @unique() @map("permission_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
@@map("permission")
|
||||
}
|
||||
|
||||
model Role {
|
||||
id String @id() @unique() @map("role_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(255)
|
||||
description String? @db.VarChar(255)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
groupRoles GroupRole[]
|
||||
userRoles UserRole[]
|
||||
|
||||
@@map("role")
|
||||
}
|
||||
|
||||
model UserRole {
|
||||
id String @id() @unique() @map("user_role_id") @db.Uuid
|
||||
roleId String @map("role_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
role Role @relation(fields: [roleId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("user_role")
|
||||
}
|
||||
|
||||
model Team {
|
||||
id String @id() @unique() @map("team_id") @db.Uuid
|
||||
name String @unique() @db.VarChar(50)
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
teamWebsites TeamWebsite[]
|
||||
teamUsers TeamUser[]
|
||||
|
||||
@@map("team")
|
||||
}
|
||||
|
||||
model TeamWebsite {
|
||||
id String @id() @unique() @map("team_website_id") @db.Uuid
|
||||
teamId String @map("team_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
user User? @relation(fields: [userId], references: [id])
|
||||
userId String? @db.Uuid
|
||||
|
||||
@@map("team_website")
|
||||
}
|
||||
|
||||
model TeamUser {
|
||||
id String @id() @unique() @map("team_user_id") @db.Uuid
|
||||
teamId String @map("team_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
isDeleted Boolean @default(false) @map("is_deleted")
|
||||
|
||||
team Team @relation(fields: [teamId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("team_user")
|
||||
}
|
||||
|
||||
model UserWebsite {
|
||||
id String @id() @unique() @map("user_website_id") @db.Uuid
|
||||
userId String @map("user_id") @db.Uuid
|
||||
websiteId String @map("website_id") @db.Uuid
|
||||
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
|
||||
|
||||
website Website @relation(fields: [websiteId], references: [id])
|
||||
user User @relation(fields: [userId], references: [id])
|
||||
|
||||
@@map("user_website")
|
||||
}
|
||||
|
@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
||||
import useStore, { setConfig } from 'store/app';
|
||||
import useApi from 'hooks/useApi';
|
||||
|
||||
let fetched = false;
|
||||
let loading = false;
|
||||
|
||||
export default function useConfig() {
|
||||
const { config } = useStore();
|
||||
@ -10,12 +10,13 @@ export default function useConfig() {
|
||||
|
||||
async function loadConfig() {
|
||||
const { data } = await get('/config');
|
||||
loading = false;
|
||||
setConfig(data);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!config && !fetched) {
|
||||
fetched = true;
|
||||
if (!config && !loading) {
|
||||
loading = true;
|
||||
loadConfig();
|
||||
}
|
||||
}, []);
|
||||
|
@ -12,14 +12,17 @@ export default function useRequireLogin() {
|
||||
async function loadUser() {
|
||||
setLoading(true);
|
||||
|
||||
const { ok, data } = await get('/auth/verify');
|
||||
const {
|
||||
ok,
|
||||
data: { user },
|
||||
} = await get('/auth/verify');
|
||||
|
||||
if (!ok) {
|
||||
await router.push('/login');
|
||||
return null;
|
||||
}
|
||||
|
||||
setUser(data);
|
||||
setUser(user);
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
5
interface/auth.d.ts
vendored
Normal file
5
interface/auth.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Auth {
|
||||
id: number;
|
||||
email?: string;
|
||||
teams?: string[];
|
||||
}
|
22
interface/base.d.ts
vendored
Normal file
22
interface/base.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
import { Auth } from './auth';
|
||||
|
||||
export interface NextApiRequestQueryBody<TQuery, TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface NextApiRequestQuery<TQuery> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
}
|
||||
|
||||
export interface NextApiRequestBody<TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface ObjectAny {
|
||||
[key: string]: any;
|
||||
}
|
22
interface/index.d.ts
vendored
Normal file
22
interface/index.d.ts
vendored
Normal file
@ -0,0 +1,22 @@
|
||||
import { NextApiRequest } from 'next';
|
||||
import { Auth } from './auth';
|
||||
|
||||
export interface NextApiRequestQueryBody<TQuery, TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface NextApiRequestQuery<TQuery> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
query: TQuery;
|
||||
}
|
||||
|
||||
export interface NextApiRequestBody<TBody> extends NextApiRequest {
|
||||
auth: Auth;
|
||||
body: TBody;
|
||||
}
|
||||
|
||||
export interface ObjectAny {
|
||||
[key: string]: any;
|
||||
}
|
41
lib/auth.js
41
lib/auth.js
@ -1,22 +1,31 @@
|
||||
import { parseSecureToken, parseToken } from 'next-basics';
|
||||
import { getAccount, getWebsite } from 'queries';
|
||||
import { SHARE_TOKEN_HEADER, TYPE_ACCOUNT, TYPE_WEBSITE } from 'lib/constants';
|
||||
import { getUser, getWebsite } from 'queries';
|
||||
import debug from 'debug';
|
||||
import { SHARE_TOKEN_HEADER, TYPE_USER, TYPE_WEBSITE } from 'lib/constants';
|
||||
import { secret } from 'lib/crypto';
|
||||
|
||||
export function getAuthToken(req) {
|
||||
try {
|
||||
const token = req.headers.authorization;
|
||||
const log = debug('umami:auth');
|
||||
|
||||
return parseSecureToken(token.split(' ')[1], secret());
|
||||
} catch {
|
||||
export function getAuthToken(req) {
|
||||
const token = req.headers.authorization;
|
||||
|
||||
return token.split(' ')[1];
|
||||
}
|
||||
|
||||
export function parseAuthToken(req) {
|
||||
try {
|
||||
return parseSecureToken(getAuthToken(req), secret());
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getShareToken(req) {
|
||||
export function parseShareToken(req) {
|
||||
try {
|
||||
return parseToken(req.headers[SHARE_TOKEN_HEADER], secret());
|
||||
} catch {
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -29,6 +38,7 @@ export function isValidToken(token, validation) {
|
||||
return validation(token);
|
||||
}
|
||||
} catch (e) {
|
||||
log(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -38,7 +48,10 @@ export function isValidToken(token, validation) {
|
||||
export async function allowQuery(req, type) {
|
||||
const { id } = req.query;
|
||||
|
||||
const { userId, isAdmin, shareToken } = req.auth ?? {};
|
||||
const {
|
||||
user: { id: userId, isAdmin },
|
||||
shareToken,
|
||||
} = req.auth;
|
||||
|
||||
if (isAdmin) {
|
||||
return true;
|
||||
@ -50,13 +63,13 @@ export async function allowQuery(req, type) {
|
||||
|
||||
if (userId) {
|
||||
if (type === TYPE_WEBSITE) {
|
||||
const website = await getWebsite({ websiteUuid: id });
|
||||
const website = await getWebsite({ id });
|
||||
|
||||
return website && website.userId === userId;
|
||||
} else if (type === TYPE_ACCOUNT) {
|
||||
const account = await getAccount({ accountUuid: id });
|
||||
} else if (type === TYPE_USER) {
|
||||
const user = await getUser({ id });
|
||||
|
||||
return account && account.accountUuid === id;
|
||||
return user && user.id === id;
|
||||
}
|
||||
}
|
||||
|
||||
|
84
lib/cache.js
Normal file
84
lib/cache.js
Normal file
@ -0,0 +1,84 @@
|
||||
import { getWebsite, getUser, getSession } from '../queries';
|
||||
import redis, { DELETED } from 'lib/redis';
|
||||
|
||||
async function fetchObject(key, query) {
|
||||
const obj = await redis.get(key);
|
||||
|
||||
if (!obj) {
|
||||
return query().then(async data => {
|
||||
if (data) {
|
||||
await redis.set(key, data);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
async function storeObject(key, data) {
|
||||
return redis.set(key, data);
|
||||
}
|
||||
|
||||
async function deleteObject(key) {
|
||||
return redis.set(key, DELETED);
|
||||
}
|
||||
|
||||
async function fetchWebsite(id) {
|
||||
return fetchObject(`website:${id}`, () => getWebsite({ id }));
|
||||
}
|
||||
|
||||
async function storeWebsite(data) {
|
||||
const { id } = data;
|
||||
const key = `website:${id}`;
|
||||
|
||||
return storeObject(key, data);
|
||||
}
|
||||
|
||||
async function deleteWebsite(id) {
|
||||
return deleteObject(`website:${id}`);
|
||||
}
|
||||
|
||||
async function fetchUser(id) {
|
||||
return fetchObject(`user:${id}`, () => getUser({ id }));
|
||||
}
|
||||
|
||||
async function storeUser(data) {
|
||||
const { id } = data;
|
||||
const key = `user:${id}`;
|
||||
|
||||
return storeObject(key, data);
|
||||
}
|
||||
|
||||
async function deleteUser(id) {
|
||||
return deleteObject(`user:${id}`);
|
||||
}
|
||||
|
||||
async function fetchSession(id) {
|
||||
return fetchObject(`session:${id}`, () => getSession({ id }));
|
||||
}
|
||||
|
||||
async function storeSession(data) {
|
||||
const { id } = data;
|
||||
const key = `session:${id}`;
|
||||
|
||||
return storeObject(key, data);
|
||||
}
|
||||
|
||||
async function deleteSession(id) {
|
||||
return deleteObject(`session:${id}`);
|
||||
}
|
||||
|
||||
export default {
|
||||
fetchWebsite,
|
||||
storeWebsite,
|
||||
deleteWebsite,
|
||||
fetchUser,
|
||||
storeUser,
|
||||
deleteUser,
|
||||
fetchSession,
|
||||
storeSession,
|
||||
deleteSession,
|
||||
enabled: redis.enabled,
|
||||
};
|
@ -22,7 +22,7 @@ export const REALTIME_RANGE = 30;
|
||||
export const REALTIME_INTERVAL = 3000;
|
||||
|
||||
export const TYPE_WEBSITE = 'website';
|
||||
export const TYPE_ACCOUNT = 'account';
|
||||
export const TYPE_USER = 'user';
|
||||
|
||||
export const THEME_COLORS = {
|
||||
light: {
|
||||
|
@ -9,11 +9,11 @@ export function secret() {
|
||||
export function salt() {
|
||||
const ROTATING_SALT = hash(startOfMonth(new Date()).toUTCString());
|
||||
|
||||
return hash([secret(), ROTATING_SALT]);
|
||||
return hash(secret(), ROTATING_SALT);
|
||||
}
|
||||
|
||||
export function uuid(...args) {
|
||||
if (!args.length) return v4();
|
||||
|
||||
return v5(hash([...args, salt()]), v5.DNS);
|
||||
return v5(hash(...args, salt()), v5.DNS);
|
||||
}
|
||||
|
@ -1,23 +1,22 @@
|
||||
import { createMiddleware, unauthorized, badRequest, serverError } from 'next-basics';
|
||||
import { createMiddleware, unauthorized, badRequest, parseSecureToken } from 'next-basics';
|
||||
import debug from 'debug';
|
||||
import cors from 'cors';
|
||||
import { getSession } from './session';
|
||||
import { getAuthToken, getShareToken } from './auth';
|
||||
import { validate } from 'uuid';
|
||||
import { findSession } from 'lib/session';
|
||||
import { parseShareToken, getAuthToken } from 'lib/auth';
|
||||
import { secret } from 'lib/crypto';
|
||||
import redis from 'lib/redis';
|
||||
import { getUser } from '../queries';
|
||||
|
||||
const log = debug('umami:middleware');
|
||||
|
||||
export const useCors = createMiddleware(cors());
|
||||
|
||||
export const useSession = createMiddleware(async (req, res, next) => {
|
||||
let session;
|
||||
|
||||
try {
|
||||
session = await getSession(req);
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(e);
|
||||
|
||||
return serverError(res, e.message);
|
||||
}
|
||||
const session = await findSession(req);
|
||||
|
||||
if (!session) {
|
||||
log('useSession:session-not-found');
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
@ -26,13 +25,25 @@ 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);
|
||||
const token = getAuthToken(req);
|
||||
const key = parseSecureToken(token, secret());
|
||||
const shareToken = await parseShareToken(req);
|
||||
|
||||
if (!token && !shareToken) {
|
||||
let user;
|
||||
|
||||
if (validate(key)) {
|
||||
user = await getUser({ id: key });
|
||||
} else if (redis.enabled) {
|
||||
user = await redis.get(key);
|
||||
}
|
||||
|
||||
if (!user && !shareToken) {
|
||||
log('useAuth:user-not-authorized');
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
req.auth = { ...token, shareToken };
|
||||
log({ user, token, shareToken, key });
|
||||
|
||||
req.auth = { user, token, shareToken, key };
|
||||
next();
|
||||
});
|
||||
|
51
lib/redis.js
51
lib/redis.js
@ -1,18 +1,15 @@
|
||||
import Redis from 'ioredis';
|
||||
import { startOfMonth } from 'date-fns';
|
||||
import debug from 'debug';
|
||||
import { getSessions, getAllWebsites } from 'queries';
|
||||
import Redis from 'ioredis';
|
||||
import { REDIS } from 'lib/db';
|
||||
|
||||
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) {
|
||||
if (!enabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -32,48 +29,34 @@ function getClient() {
|
||||
return redis;
|
||||
}
|
||||
|
||||
async function stageData() {
|
||||
const sessions = await getSessions([], startOfMonth(new Date()));
|
||||
const websites = await getAllWebsites();
|
||||
|
||||
const sessionUuids = sessions.map(a => {
|
||||
return { key: `session:${a.sessionUuid}`, value: 1 };
|
||||
});
|
||||
const websiteIds = websites.map(a => {
|
||||
return { key: `website:${a.websiteUuid}`, value: Number(a.websiteId) };
|
||||
});
|
||||
|
||||
await addSet(sessionUuids);
|
||||
await addSet(websiteIds);
|
||||
|
||||
await redis.set(INITIALIZED, 1);
|
||||
}
|
||||
|
||||
async function addSet(ids) {
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
const { key, value } = ids[i];
|
||||
await redis.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
async function get(key) {
|
||||
await connect();
|
||||
|
||||
return redis.get(key);
|
||||
try {
|
||||
return JSON.parse(await redis.get(key));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function set(key, value) {
|
||||
await connect();
|
||||
|
||||
return redis.set(key, value);
|
||||
return redis.set(key, JSON.stringify(value));
|
||||
}
|
||||
|
||||
async function del(key) {
|
||||
await connect();
|
||||
|
||||
return redis.del(key);
|
||||
}
|
||||
|
||||
async function connect() {
|
||||
if (!redis) {
|
||||
redis = process.env.REDIS_URL && (global[REDIS] || getClient());
|
||||
if (!redis && enabled) {
|
||||
redis = global[REDIS] || getClient();
|
||||
}
|
||||
|
||||
return redis;
|
||||
}
|
||||
|
||||
export default { enabled, client: redis, log, connect, get, set, stageData };
|
||||
export default { enabled, client: redis, log, connect, get, set, del };
|
||||
|
124
lib/session.js
124
lib/session.js
@ -1,106 +1,96 @@
|
||||
import { parseToken } from 'next-basics';
|
||||
import { validate } from 'uuid';
|
||||
import { secret, uuid } from 'lib/crypto';
|
||||
import redis, { DELETED } from 'lib/redis';
|
||||
import cache from 'lib/cache';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { getClientInfo, getJsonBody } from 'lib/request';
|
||||
import { createSession, getSessionByUuid, getWebsite } from 'queries';
|
||||
import { createSession, getSession, getWebsite } from 'queries';
|
||||
|
||||
export async function getSession(req) {
|
||||
export async function findSession(req) {
|
||||
const { payload } = getJsonBody(req);
|
||||
|
||||
if (!payload) {
|
||||
throw new Error('Invalid request');
|
||||
return null;
|
||||
}
|
||||
|
||||
const cache = req.headers['x-umami-cache'];
|
||||
// Check if cache token is passed
|
||||
const cacheToken = req.headers['x-umami-cache'];
|
||||
|
||||
if (cache) {
|
||||
const result = await parseToken(cache, secret());
|
||||
if (cacheToken) {
|
||||
const result = await parseToken(cacheToken, secret());
|
||||
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
const { website: websiteUuid, hostname, screen, language } = payload;
|
||||
// Verify payload
|
||||
const { website: websiteId, hostname, screen, language } = payload;
|
||||
|
||||
if (!validate(websiteUuid)) {
|
||||
if (!validate(websiteId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let websiteId = null;
|
||||
// Find website
|
||||
let website;
|
||||
|
||||
// Check if website exists
|
||||
if (redis.enabled) {
|
||||
websiteId = Number(await redis.get(`website:${websiteUuid}`));
|
||||
if (cache.enabled) {
|
||||
website = await cache.fetchWebsite(websiteId);
|
||||
} else {
|
||||
website = await getWebsite({ id: websiteId });
|
||||
}
|
||||
|
||||
// Check database if does not exists in Redis
|
||||
if (!websiteId) {
|
||||
const website = await getWebsite({ websiteUuid });
|
||||
websiteId = website ? website.id : null;
|
||||
}
|
||||
|
||||
if (!websiteId || websiteId === DELETED) {
|
||||
throw new Error(`Website not found: ${websiteUuid}`);
|
||||
if (!website || website.isDeleted) {
|
||||
throw new Error(`Website not found: ${websiteId}`);
|
||||
}
|
||||
|
||||
const { userAgent, browser, os, ip, country, device } = await getClientInfo(req, payload);
|
||||
const sessionUuid = uuid(websiteUuid, hostname, ip, userAgent);
|
||||
const sessionId = uuid(websiteId, hostname, ip, userAgent);
|
||||
|
||||
let sessionId = null;
|
||||
let session = null;
|
||||
|
||||
if (!clickhouse.enabled) {
|
||||
// Check if session exists
|
||||
if (redis.enabled) {
|
||||
sessionId = Number(await redis.get(`session:${sessionUuid}`));
|
||||
}
|
||||
|
||||
// Check database if does not exists in Redis
|
||||
if (!sessionId) {
|
||||
session = await getSessionByUuid(sessionUuid);
|
||||
sessionId = session ? session.id : null;
|
||||
}
|
||||
|
||||
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,
|
||||
// Clickhouse does not require session lookup
|
||||
if (clickhouse.enabled) {
|
||||
return {
|
||||
id: sessionId,
|
||||
websiteId,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
device,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
website: {
|
||||
websiteId,
|
||||
websiteUuid,
|
||||
},
|
||||
session,
|
||||
};
|
||||
// Find session
|
||||
let session;
|
||||
|
||||
if (cache.enabled) {
|
||||
session = await cache.fetchSession(sessionId);
|
||||
} else {
|
||||
session = await getSession({ id: sessionId });
|
||||
}
|
||||
|
||||
// Create a session if not found
|
||||
if (!session) {
|
||||
try {
|
||||
session = await createSession({
|
||||
id: sessionId,
|
||||
websiteId,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country,
|
||||
});
|
||||
} catch (e) {
|
||||
if (!e.message.toLowerCase().includes('unique constraint')) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
5
next-env.d.ts
vendored
Normal file
5
next-env.d.ts
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
@ -36,7 +36,7 @@ module.exports = {
|
||||
env: {
|
||||
currentVersion: pkg.version,
|
||||
isProduction: process.env.NODE_ENV === 'production',
|
||||
isCloudMode: process.env.CLOUD_MODE,
|
||||
uiDisabled: !!process.env.DISABLE_UI,
|
||||
},
|
||||
basePath: process.env.BASE_PATH,
|
||||
output: 'standalone',
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "umami",
|
||||
"version": "1.39.4",
|
||||
"version": "2.0.0-beta.2",
|
||||
"description": "A simple, fast, privacy-focused alternative to Google Analytics.",
|
||||
"author": "Mike Cao <mike@mikecao.com>",
|
||||
"license": "MIT",
|
||||
@ -84,7 +84,7 @@
|
||||
"maxmind": "^4.3.6",
|
||||
"moment-timezone": "^0.5.35",
|
||||
"next": "^12.3.1",
|
||||
"next-basics": "^0.18.0",
|
||||
"next-basics": "^0.23.0",
|
||||
"node-fetch": "^3.2.8",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prop-types": "^15.7.2",
|
||||
|
@ -2,30 +2,27 @@ import Head from 'next/head';
|
||||
import { useRouter } from 'next/router';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import useLocale from 'hooks/useLocale';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
import 'styles/variables.css';
|
||||
import 'styles/bootstrap-grid.css';
|
||||
import 'styles/index.css';
|
||||
import '@fontsource/inter/400.css';
|
||||
import '@fontsource/inter/600.css';
|
||||
|
||||
const Intl = ({ children }) => {
|
||||
export default function App({ Component, pageProps }) {
|
||||
const { locale, messages } = useLocale();
|
||||
const { basePath } = useRouter();
|
||||
const { dir } = useLocale();
|
||||
useConfig();
|
||||
|
||||
const Wrapper = ({ children }) => <span className={locale}>{children}</span>;
|
||||
|
||||
if (process.env.uiDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<IntlProvider locale={locale} messages={messages[locale]} textComponent={Wrapper}>
|
||||
{children}
|
||||
</IntlProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default function App({ Component, pageProps }) {
|
||||
const { basePath } = useRouter();
|
||||
const { dir } = useLocale();
|
||||
|
||||
return (
|
||||
<Intl>
|
||||
<Head>
|
||||
<link rel="icon" href={`${basePath}/favicon.ico`} />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href={`${basePath}/apple-touch-icon.png`} />
|
||||
@ -41,6 +38,6 @@ export default function App({ Component, pageProps }) {
|
||||
<div className="container" dir={dir}>
|
||||
<Component {...pageProps} />
|
||||
</div>
|
||||
</Intl>
|
||||
</IntlProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1,23 +1,44 @@
|
||||
import { ok, unauthorized, badRequest, checkPassword, createSecureToken } from 'next-basics';
|
||||
import { getAccount } from 'queries';
|
||||
import {
|
||||
ok,
|
||||
unauthorized,
|
||||
badRequest,
|
||||
checkPassword,
|
||||
createSecureToken,
|
||||
methodNotAllowed,
|
||||
getRandomChars,
|
||||
} from 'next-basics';
|
||||
import { getUser } from 'queries';
|
||||
import { secret } from 'lib/crypto';
|
||||
import redis from 'lib/redis';
|
||||
|
||||
export default async (req, res) => {
|
||||
const { username, password } = req.body;
|
||||
if (req.method === 'POST') {
|
||||
const { username, password } = req.body;
|
||||
|
||||
if (!username || !password) {
|
||||
return badRequest(res);
|
||||
if (!username || !password) {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const user = await getUser({ username });
|
||||
|
||||
if (user && checkPassword(password, user.password)) {
|
||||
if (redis.enabled) {
|
||||
const key = `auth:${getRandomChars(32)}`;
|
||||
|
||||
await redis.set(key, user);
|
||||
|
||||
const token = createSecureToken(key, secret());
|
||||
|
||||
return ok(res, { token, user });
|
||||
}
|
||||
|
||||
const token = createSecureToken(user.id, secret());
|
||||
|
||||
return ok(res, { token, user });
|
||||
}
|
||||
|
||||
return unauthorized(res, 'Incorrect username and/or password.');
|
||||
}
|
||||
|
||||
const account = await getAccount({ username });
|
||||
|
||||
if (account && checkPassword(password, account.password)) {
|
||||
const { id, username, isAdmin, accountUuid } = account;
|
||||
const user = { userId: id, username, isAdmin, accountUuid };
|
||||
const token = createSecureToken(user, secret());
|
||||
|
||||
return ok(res, { token, user });
|
||||
}
|
||||
|
||||
return unauthorized(res);
|
||||
return methodNotAllowed(res);
|
||||
};
|
||||
|
18
pages/api/auth/logout.js
Normal file
18
pages/api/auth/logout.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { methodNotAllowed, ok } from 'next-basics';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import redis from 'lib/redis';
|
||||
import { getAuthToken } from 'lib/auth';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'POST') {
|
||||
if (redis.enabled) {
|
||||
await redis.del(getAuthToken(req));
|
||||
}
|
||||
|
||||
return ok(res);
|
||||
}
|
||||
|
||||
return methodNotAllowed(res);
|
||||
};
|
@ -1,12 +1,8 @@
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { ok, unauthorized } from 'next-basics';
|
||||
import { ok } from 'next-basics';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.auth) {
|
||||
return ok(res, req.auth);
|
||||
}
|
||||
|
||||
return unauthorized(res);
|
||||
return ok(res, req.auth);
|
||||
};
|
||||
|
@ -5,7 +5,7 @@ import { createToken, unauthorized, send, badRequest, forbidden } from 'next-bas
|
||||
import { savePageView, saveEvent } from 'queries';
|
||||
import { useCors, useSession } from 'lib/middleware';
|
||||
import { getJsonBody, getIpAddress } from 'lib/request';
|
||||
import { secret, uuid } from 'lib/crypto';
|
||||
import { secret } from 'lib/crypto';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useCors(req, res);
|
||||
@ -58,7 +58,7 @@ export default async (req, res) => {
|
||||
|
||||
await useSession(req, res);
|
||||
|
||||
const { website, session } = req.session;
|
||||
const session = req.session;
|
||||
|
||||
const { type, payload } = getJsonBody(req);
|
||||
|
||||
@ -68,14 +68,11 @@ export default async (req, res) => {
|
||||
url = url.replace(/\/$/, '');
|
||||
}
|
||||
|
||||
const eventUuid = uuid();
|
||||
|
||||
if (type === 'pageview') {
|
||||
await savePageView(website, { session, url, referrer });
|
||||
await savePageView({ ...session, url, referrer });
|
||||
} else if (type === 'event') {
|
||||
await saveEvent(website, {
|
||||
session,
|
||||
eventUuid,
|
||||
await saveEvent({
|
||||
...session,
|
||||
url,
|
||||
eventName,
|
||||
eventData,
|
||||
@ -84,13 +81,7 @@ export default async (req, res) => {
|
||||
return badRequest(res);
|
||||
}
|
||||
|
||||
const token = createToken(
|
||||
{
|
||||
website,
|
||||
session,
|
||||
},
|
||||
secret(),
|
||||
);
|
||||
const token = createToken(session, secret());
|
||||
|
||||
return send(res, token);
|
||||
};
|
||||
|
@ -7,6 +7,7 @@ export default async (req, res) => {
|
||||
trackerScriptName: process.env.TRACKER_SCRIPT_NAME,
|
||||
updatesDisabled: !!process.env.DISABLE_UPDATES,
|
||||
telemetryDisabled: !!process.env.DISABLE_TELEMETRY,
|
||||
adminDisabled: !!process.env.DISABLE_ADMIN,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -8,10 +8,10 @@ export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const { userId } = req.auth;
|
||||
const { id: userId } = req.auth.user;
|
||||
|
||||
const websites = await getUserWebsites({ userId });
|
||||
const ids = websites.map(({ websiteUuid }) => websiteUuid);
|
||||
const websites = await getUserWebsites(userId);
|
||||
const ids = websites.map(({ id }) => id);
|
||||
const token = createToken({ websites: ids }, secret());
|
||||
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));
|
||||
|
||||
|
@ -3,14 +3,14 @@ import { ok, notFound, methodNotAllowed, createToken } from 'next-basics';
|
||||
import { secret } from 'lib/crypto';
|
||||
|
||||
export default async (req, res) => {
|
||||
const { id } = req.query;
|
||||
const { id: shareId } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const website = await getWebsite({ shareId: id });
|
||||
const website = await getWebsite({ shareId });
|
||||
|
||||
if (website) {
|
||||
const { websiteUuid } = website;
|
||||
const data = { id: websiteUuid };
|
||||
const { id } = website;
|
||||
const data = { id };
|
||||
const token = createToken(data, secret());
|
||||
|
||||
return ok(res, { ...data, token });
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { badRequest, hashPassword, methodNotAllowed, ok, unauthorized } from 'next-basics';
|
||||
import { getAccount, deleteAccount, updateAccount } from 'queries';
|
||||
import { getUser, deleteUser, updateUser } from 'queries';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
const { isAdmin, userId } = req.auth;
|
||||
const {
|
||||
user: { id: userId, isAdmin },
|
||||
} = req.auth;
|
||||
const { id } = req.query;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
@ -13,9 +15,9 @@ export default async (req, res) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const account = await getAccount({ id: +id });
|
||||
const user = await getUser({ id });
|
||||
|
||||
return ok(res, account);
|
||||
return ok(res, user);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
@ -25,7 +27,7 @@ export default async (req, res) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
const account = await getAccount({ id: +id });
|
||||
const user = await getUser({ id });
|
||||
|
||||
const data = {};
|
||||
|
||||
@ -39,29 +41,29 @@ export default async (req, res) => {
|
||||
}
|
||||
|
||||
// Check when username changes
|
||||
if (data.username && account.username !== data.username) {
|
||||
const accountByUsername = await getAccount({ username });
|
||||
if (data.username && user.username !== data.username) {
|
||||
const userByUsername = await getUser({ username });
|
||||
|
||||
if (accountByUsername) {
|
||||
return badRequest(res, 'Account already exists.');
|
||||
if (userByUsername) {
|
||||
return badRequest(res, 'User already exists');
|
||||
}
|
||||
}
|
||||
|
||||
const updated = await updateAccount(data, { id: +id });
|
||||
const updated = await updateUser(data, { id });
|
||||
|
||||
return ok(res, updated);
|
||||
}
|
||||
|
||||
if (req.method === 'DELETE') {
|
||||
if (id === userId) {
|
||||
return badRequest(res, 'You cannot delete your own account.');
|
||||
return badRequest(res, 'You cannot delete your own user.');
|
||||
}
|
||||
|
||||
if (!isAdmin) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
await deleteAccount(+id);
|
||||
await deleteUser(id);
|
||||
|
||||
return ok(res);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { getAccount, updateAccount } from 'queries';
|
||||
import { getUser, updateUser } from 'queries';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import {
|
||||
badRequest,
|
||||
@ -9,28 +9,28 @@ import {
|
||||
hashPassword,
|
||||
} from 'next-basics';
|
||||
import { allowQuery } from 'lib/auth';
|
||||
import { TYPE_ACCOUNT } from 'lib/constants';
|
||||
import { TYPE_USER } from 'lib/constants';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
const { current_password, new_password } = req.body;
|
||||
const { id: accountUuid } = req.query;
|
||||
const { id } = req.query;
|
||||
|
||||
if (!(await allowQuery(req, TYPE_ACCOUNT))) {
|
||||
if (!(await allowQuery(req, TYPE_USER))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const account = await getAccount({ accountUuid });
|
||||
const user = await getUser({ id });
|
||||
|
||||
if (!checkPassword(current_password, account.password)) {
|
||||
if (!checkPassword(current_password, user.password)) {
|
||||
return badRequest(res, 'Current password is incorrect');
|
||||
}
|
||||
|
||||
const password = hashPassword(new_password);
|
||||
|
||||
const updated = await updateAccount({ password }, { accountUuid });
|
||||
const updated = await updateUser({ password }, { id });
|
||||
|
||||
return ok(res, updated);
|
||||
}
|
@ -1,36 +1,38 @@
|
||||
import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import { createAccount, getAccount, getAccounts } from 'queries';
|
||||
import { createUser, getUser, getUsers } from 'queries';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useAuth(req, res);
|
||||
|
||||
const { isAdmin } = req.auth;
|
||||
const {
|
||||
user: { isAdmin },
|
||||
} = req.auth;
|
||||
|
||||
if (!isAdmin) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const accounts = await getAccounts();
|
||||
const users = await getUsers();
|
||||
|
||||
return ok(res, accounts);
|
||||
return ok(res, users);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const { username, password, account_uuid } = req.body;
|
||||
const { username, password, id } = req.body;
|
||||
|
||||
const account = await getAccount({ username });
|
||||
const user = await getUser({ username });
|
||||
|
||||
if (account) {
|
||||
return badRequest(res, 'Account already exists');
|
||||
if (user) {
|
||||
return badRequest(res, 'User already exists');
|
||||
}
|
||||
|
||||
const created = await createAccount({
|
||||
const created = await createUser({
|
||||
id: id || uuid(),
|
||||
username,
|
||||
password: hashPassword(password),
|
||||
accountUuid: account_uuid || uuid(),
|
||||
});
|
||||
|
||||
return ok(res, created);
|
@ -1,53 +1,34 @@
|
||||
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';
|
||||
import { methodNotAllowed, ok, serverError, unauthorized } from 'next-basics';
|
||||
import { deleteWebsite, getWebsite, updateWebsite } from 'queries';
|
||||
import { TYPE_WEBSITE } from 'lib/constants';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
const { id: websiteUuid } = req.query;
|
||||
const { id: websiteId } = req.query;
|
||||
|
||||
if (!(await allowQuery(req, TYPE_WEBSITE))) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
if (req.method === 'GET') {
|
||||
const website = await getWebsite({ websiteUuid });
|
||||
const website = await getWebsite({ id: 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 });
|
||||
|
||||
const newShareId = enableShareUrl ? website.shareId || getRandomChars(8) : null;
|
||||
const { name, domain, shareId } = req.body;
|
||||
|
||||
try {
|
||||
await updateWebsite(
|
||||
{
|
||||
name,
|
||||
domain,
|
||||
shareId: shareId ? shareId : newShareId,
|
||||
userId: +owner || account.id,
|
||||
},
|
||||
{ websiteUuid },
|
||||
);
|
||||
await updateWebsite(websiteId, {
|
||||
name,
|
||||
domain,
|
||||
shareId,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
|
||||
return serverError(res, 'That share ID is already taken.');
|
||||
@ -62,7 +43,7 @@ export default async (req, res) => {
|
||||
return unauthorized(res);
|
||||
}
|
||||
|
||||
await deleteWebsite(websiteUuid);
|
||||
await deleteWebsite(websiteId);
|
||||
|
||||
return ok(res);
|
||||
}
|
||||
|
@ -94,7 +94,7 @@ export default async (req, res) => {
|
||||
let domain;
|
||||
|
||||
if (type === 'referrer') {
|
||||
const website = await getWebsite({ websiteUuid: websiteId });
|
||||
const website = await getWebsite({ id: websiteId });
|
||||
|
||||
if (!website) {
|
||||
return badRequest(res);
|
||||
|
@ -1,47 +1,30 @@
|
||||
import { createWebsite, getAccount, getAllWebsites, getUserWebsites } from 'queries';
|
||||
import { ok, methodNotAllowed, unauthorized, getRandomChars } from 'next-basics';
|
||||
import { useAuth } from 'lib/middleware';
|
||||
import { createWebsite, getAllWebsites, getUserWebsites } from 'queries';
|
||||
import { ok, methodNotAllowed, getRandomChars } from 'next-basics';
|
||||
import { useAuth, useCors } from 'lib/middleware';
|
||||
import { uuid } from 'lib/crypto';
|
||||
|
||||
export default async (req, res) => {
|
||||
await useCors(req, res);
|
||||
await useAuth(req, res);
|
||||
|
||||
const { user_id, include_all } = req.query;
|
||||
const { userId: currentUserId, isAdmin } = req.auth;
|
||||
const accountUuid = user_id || req.auth.accountUuid;
|
||||
let account;
|
||||
|
||||
if (accountUuid) {
|
||||
account = await getAccount({ accountUuid });
|
||||
}
|
||||
|
||||
const userId = account ? account.id : user_id;
|
||||
const {
|
||||
user: { id: userId, isAdmin },
|
||||
} = req.auth;
|
||||
|
||||
if (req.method === 'GET') {
|
||||
if (userId && userId !== currentUserId && !isAdmin) {
|
||||
return unauthorized(res);
|
||||
}
|
||||
const { include_all } = req.query;
|
||||
|
||||
const websites =
|
||||
isAdmin && include_all
|
||||
? await getAllWebsites()
|
||||
: await getUserWebsites({ userId: account?.id });
|
||||
isAdmin && include_all ? await getAllWebsites() : await getUserWebsites(userId);
|
||||
|
||||
return ok(res, websites);
|
||||
}
|
||||
|
||||
if (req.method === 'POST') {
|
||||
const { name, domain, owner, enableShareUrl } = req.body;
|
||||
const { name, domain, 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 });
|
||||
const website = await createWebsite(userId, { id: uuid(), name, domain, shareId });
|
||||
|
||||
return ok(res, website);
|
||||
}
|
||||
|
@ -4,11 +4,11 @@ import TestConsole from 'components/pages/TestConsole';
|
||||
import useRequireLogin from 'hooks/useRequireLogin';
|
||||
import useUser from 'hooks/useUser';
|
||||
|
||||
export default function ConsolePage({ enabled }) {
|
||||
export default function ConsolePage({ pageDisabled }) {
|
||||
const { loading } = useRequireLogin();
|
||||
const { user } = useUser();
|
||||
|
||||
if (loading || !enabled || !user?.isAdmin) {
|
||||
if (pageDisabled || loading || !user?.isAdmin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -21,6 +21,8 @@ export default function ConsolePage({ enabled }) {
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return {
|
||||
props: { enabled: !!process.env.ENABLE_TEST_CONSOLE },
|
||||
props: {
|
||||
pageDisabled: !process.env.ENABLE_TEST_CONSOLE,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,9 +1,10 @@
|
||||
import React from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import Layout from 'components/layout/Layout';
|
||||
import Dashboard from 'components/pages/Dashboard';
|
||||
import useRequireLogin from 'hooks/useRequireLogin';
|
||||
import { useRouter } from 'next/router';
|
||||
import useUser from 'hooks/useUser';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
export default function DashboardPage() {
|
||||
const {
|
||||
@ -13,8 +14,9 @@ export default function DashboardPage() {
|
||||
} = useRouter();
|
||||
const { loading } = useRequireLogin();
|
||||
const user = useUser();
|
||||
const { adminDisabled } = useConfig();
|
||||
|
||||
if (!user || !isReady || loading) {
|
||||
if (adminDisabled || !user || !isReady || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,8 @@ import React from 'react';
|
||||
import Layout from 'components/layout/Layout';
|
||||
import LoginForm from 'components/forms/LoginForm';
|
||||
|
||||
export default function LoginPage({ loginDisabled }) {
|
||||
if (loginDisabled) {
|
||||
export default function LoginPage({ pageDisabled }) {
|
||||
if (pageDisabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -16,6 +16,8 @@ export default function LoginPage({ loginDisabled }) {
|
||||
|
||||
export async function getServerSideProps() {
|
||||
return {
|
||||
props: { loginDisabled: !!process.env.DISABLE_LOGIN || !!process.env.isCloudMode },
|
||||
props: {
|
||||
pageDisabled: !!process.env.DISABLE_LOGIN,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -1,14 +1,22 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { removeItem } from 'next-basics';
|
||||
import { removeItem, useApi } from 'next-basics';
|
||||
import { AUTH_TOKEN } from 'lib/constants';
|
||||
import { setUser } from 'store/app';
|
||||
|
||||
export default function LogoutPage() {
|
||||
const router = useRouter();
|
||||
const { post } = useApi();
|
||||
|
||||
useEffect(() => {
|
||||
async function logout() {
|
||||
await post('/logout');
|
||||
}
|
||||
|
||||
removeItem(AUTH_TOKEN);
|
||||
|
||||
logout();
|
||||
|
||||
router.push('/login');
|
||||
|
||||
return () => setUser(null);
|
||||
|
@ -2,11 +2,13 @@ import React from 'react';
|
||||
import Layout from 'components/layout/Layout';
|
||||
import Settings from 'components/pages/Settings';
|
||||
import useRequireLogin from 'hooks/useRequireLogin';
|
||||
import useConfig from 'hooks/useConfig';
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { loading } = useRequireLogin();
|
||||
const { adminDisabled } = useConfig();
|
||||
|
||||
if (process.env.isCloudMode || loading) {
|
||||
if (adminDisabled || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
19
pages/sso.js
Normal file
19
pages/sso.js
Normal file
@ -0,0 +1,19 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { setItem } from 'next-basics';
|
||||
import { AUTH_TOKEN } from 'lib/constants';
|
||||
|
||||
export default function SingleSignOnPage() {
|
||||
const router = useRouter();
|
||||
const { token, url } = router.query;
|
||||
|
||||
useEffect(() => {
|
||||
if (url && token) {
|
||||
setItem(AUTH_TOKEN, token);
|
||||
|
||||
router.push(url);
|
||||
}
|
||||
}, [router, url, token]);
|
||||
|
||||
return null;
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function createAccount(data) {
|
||||
return prisma.client.account.create({
|
||||
data,
|
||||
});
|
||||
}
|
@ -1,50 +0,0 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import redis, { DELETED } from 'lib/redis';
|
||||
|
||||
export async function deleteAccount(userId) {
|
||||
const { client } = prisma;
|
||||
|
||||
const websites = await client.website.findMany({
|
||||
where: { userId },
|
||||
select: { websiteUuid: true },
|
||||
});
|
||||
|
||||
let websiteUuids = [];
|
||||
|
||||
if (websites.length > 0) {
|
||||
websiteUuids = websites.map(a => a.websiteUuid);
|
||||
}
|
||||
|
||||
return client
|
||||
.$transaction([
|
||||
client.pageview.deleteMany({
|
||||
where: { session: { website: { userId } } },
|
||||
}),
|
||||
client.eventData.deleteMany({
|
||||
where: { event: { session: { website: { userId } } } },
|
||||
}),
|
||||
client.event.deleteMany({
|
||||
where: { session: { website: { userId } } },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { website: { userId } },
|
||||
}),
|
||||
client.website.deleteMany({
|
||||
where: { userId },
|
||||
}),
|
||||
client.account.delete({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
])
|
||||
.then(async res => {
|
||||
if (redis.enabled) {
|
||||
for (let i = 0; i < websiteUuids.length; i++) {
|
||||
await redis.set(`website:${websiteUuids[i]}`, DELETED);
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getAccount(where) {
|
||||
return prisma.client.account.findUnique({
|
||||
where,
|
||||
});
|
||||
}
|
@ -1,8 +0,0 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function updateAccount(data, where) {
|
||||
return prisma.client.account.update({
|
||||
where,
|
||||
data,
|
||||
});
|
||||
}
|
7
queries/admin/user/createUser.js
Normal file
7
queries/admin/user/createUser.js
Normal file
@ -0,0 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function createUser(data) {
|
||||
return prisma.client.user.create({
|
||||
data,
|
||||
});
|
||||
}
|
45
queries/admin/user/deleteUser.js
Normal file
45
queries/admin/user/deleteUser.js
Normal file
@ -0,0 +1,45 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function deleteUser(userId) {
|
||||
const { client } = prisma;
|
||||
|
||||
const websites = await client.website.findMany({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
let websiteIds = [];
|
||||
|
||||
if (websites.length > 0) {
|
||||
websiteIds = websites.map(a => a.id);
|
||||
}
|
||||
|
||||
return client
|
||||
.$transaction([
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId: { in: websiteIds } },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { websiteId: { in: websiteIds } },
|
||||
}),
|
||||
client.website.deleteMany({
|
||||
where: { userId },
|
||||
}),
|
||||
client.user.delete({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
}),
|
||||
])
|
||||
.then(async data => {
|
||||
if (cache.enabled) {
|
||||
const ids = websites.map(a => a.id);
|
||||
|
||||
for (let i = 0; i < ids.length; i++) {
|
||||
await cache.deleteWebsite(`website:${ids[i]}`);
|
||||
}
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
7
queries/admin/user/getUser.js
Normal file
7
queries/admin/user/getUser.js
Normal file
@ -0,0 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getUser(where) {
|
||||
return prisma.client.user.findUnique({
|
||||
where,
|
||||
});
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getAccounts() {
|
||||
return prisma.client.account.findMany({
|
||||
export async function getUsers() {
|
||||
return prisma.client.user.findMany({
|
||||
orderBy: [
|
||||
{ isAdmin: 'desc' },
|
||||
{
|
||||
@ -13,8 +13,6 @@ export async function getAccounts() {
|
||||
username: true,
|
||||
isAdmin: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
accountUuid: true,
|
||||
},
|
||||
});
|
||||
}
|
8
queries/admin/user/updateUser.js
Normal file
8
queries/admin/user/updateUser.js
Normal file
@ -0,0 +1,8 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function updateUser(data, where) {
|
||||
return prisma.client.user.update({
|
||||
where,
|
||||
data,
|
||||
});
|
||||
}
|
@ -1,11 +1,11 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import redis from 'lib/redis';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function createWebsite(userId, data) {
|
||||
return prisma.client.website
|
||||
.create({
|
||||
data: {
|
||||
account: {
|
||||
user: {
|
||||
connect: {
|
||||
id: userId,
|
||||
},
|
||||
@ -13,11 +13,11 @@ export async function createWebsite(userId, data) {
|
||||
...data,
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (redis.enabled && res) {
|
||||
await redis.set(`website:${res.websiteUuid}`, res.id);
|
||||
.then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeWebsite(data);
|
||||
}
|
||||
|
||||
return res;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
@ -1,30 +1,24 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import redis, { DELETED } from 'lib/redis';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function deleteWebsite(websiteUuid) {
|
||||
export async function deleteWebsite(id) {
|
||||
const { client, transaction } = prisma;
|
||||
|
||||
return transaction([
|
||||
client.pageview.deleteMany({
|
||||
where: { session: { website: { websiteUuid } } },
|
||||
}),
|
||||
client.eventData.deleteMany({
|
||||
where: { event: { session: { website: { websiteUuid } } } },
|
||||
}),
|
||||
client.event.deleteMany({
|
||||
where: { session: { website: { websiteUuid } } },
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId: id },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { website: { websiteUuid } },
|
||||
where: { websiteId: id },
|
||||
}),
|
||||
client.website.delete({
|
||||
where: { websiteUuid },
|
||||
where: { id },
|
||||
}),
|
||||
]).then(async res => {
|
||||
if (redis.enabled) {
|
||||
await redis.set(`website:${websiteUuid}`, DELETED);
|
||||
]).then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.deleteWebsite(id);
|
||||
}
|
||||
|
||||
return res;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ export async function getAllWebsites() {
|
||||
},
|
||||
],
|
||||
include: {
|
||||
account: {
|
||||
user: {
|
||||
select: {
|
||||
username: true,
|
||||
},
|
||||
@ -19,5 +19,5 @@ export async function getAllWebsites() {
|
||||
},
|
||||
});
|
||||
|
||||
return data.map(i => ({ ...i, account: i.account.username }));
|
||||
return data.map(i => ({ ...i, user: i.user.username }));
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getUserWebsites(where) {
|
||||
export async function getUserWebsites(userId) {
|
||||
return prisma.client.website.findMany({
|
||||
where,
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
orderBy: {
|
||||
name: 'asc',
|
||||
},
|
||||
|
@ -1,16 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import redis from 'lib/redis';
|
||||
|
||||
export async function getWebsite(where) {
|
||||
return prisma.client.website
|
||||
.findUnique({
|
||||
where,
|
||||
})
|
||||
.then(async data => {
|
||||
if (redis.enabled && data) {
|
||||
await redis.set(`website:${data.websiteUuid}`, data.id);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
return prisma.client.website.findUnique({
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
@ -1,20 +1,25 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import { getWebsite } from 'queries';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function resetWebsite(websiteId) {
|
||||
export async function resetWebsite(id) {
|
||||
const { client, transaction } = prisma;
|
||||
|
||||
const { revId } = await getWebsite({ id });
|
||||
|
||||
return transaction([
|
||||
client.pageview.deleteMany({
|
||||
where: { session: { website: { websiteUuid: websiteId } } },
|
||||
}),
|
||||
client.eventData.deleteMany({
|
||||
where: { event: { session: { website: { websiteUuid: websiteId } } } },
|
||||
}),
|
||||
client.event.deleteMany({
|
||||
where: { session: { website: { websiteUuid: websiteId } } },
|
||||
client.websiteEvent.deleteMany({
|
||||
where: { websiteId: id },
|
||||
}),
|
||||
client.session.deleteMany({
|
||||
where: { website: { websiteUuid: websiteId } },
|
||||
where: { websiteId: id },
|
||||
}),
|
||||
]);
|
||||
client.website.update({ where: { id }, data: { revId: revId + 1 } }),
|
||||
]).then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeWebsite(data[2]);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
@ -1,8 +1,10 @@
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function updateWebsite(data, where) {
|
||||
export async function updateWebsite(id, data) {
|
||||
return prisma.client.website.update({
|
||||
where,
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
data,
|
||||
});
|
||||
}
|
||||
|
@ -1,11 +1,16 @@
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import prisma from 'lib/prisma';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function getEventData(...args) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
}).then(results => {
|
||||
return Object.keys(results[0]).map(a => {
|
||||
return { x: a, y: results[0][`${a}`] };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@ -21,7 +26,7 @@ async function relationalQuery(websiteId, { startDate, endDate, event_name, colu
|
||||
on event.website_id = website.website_id
|
||||
join event_data
|
||||
on event.event_id = event_data.event_id
|
||||
where website_uuid='${websiteId}'
|
||||
where website.website_id ='${websiteId}'
|
||||
and event.created_at between $1 and $2
|
||||
${event_name ? `and event_name = ${event_name}` : ''}
|
||||
${
|
||||
@ -30,23 +35,21 @@ async function relationalQuery(websiteId, { startDate, endDate, event_name, colu
|
||||
: ''
|
||||
}`,
|
||||
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];
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const params = [websiteId, website?.revId || 0];
|
||||
|
||||
return rawQuery(
|
||||
`select
|
||||
${getEventDataColumnsQuery('event_data', columns)}
|
||||
from event
|
||||
where website_id= $1
|
||||
where website_id = $1
|
||||
and rev_id = $2
|
||||
${event_name ? `and event_name = ${event_name}` : ''}
|
||||
and ${getBetweenDates('created_at', startDate, endDate)}
|
||||
${
|
||||
@ -55,9 +58,5 @@ async function clickhouseQuery(websiteId, { startDate, endDate, event_name, colu
|
||||
: ''
|
||||
}`,
|
||||
params,
|
||||
).then(results => {
|
||||
return Object.keys(results[0]).map(a => {
|
||||
return { x: a, y: results[0][`${a}`] };
|
||||
});
|
||||
});
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function getEventMetrics(...args) {
|
||||
return runQuery({
|
||||
@ -28,7 +29,7 @@ async function relationalQuery(
|
||||
from event
|
||||
join website
|
||||
on event.website_id = website.website_id
|
||||
where website_uuid='${websiteId}'
|
||||
where website.website_id='${websiteId}'
|
||||
and event.created_at between $1 and $2
|
||||
${getFilterQuery('event', filters, params)}
|
||||
group by 1, 2
|
||||
@ -46,7 +47,8 @@ async function clickhouseQuery(
|
||||
filters = {},
|
||||
) {
|
||||
const { rawQuery, getDateQuery, getBetweenDates, getFilterQuery } = clickhouse;
|
||||
const params = [websiteId];
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const params = [websiteId, website?.revId || 0];
|
||||
|
||||
return rawQuery(
|
||||
`select
|
||||
@ -55,7 +57,8 @@ async function clickhouseQuery(
|
||||
count(*) y
|
||||
from event
|
||||
where event_name != ''
|
||||
and website_id= $1
|
||||
and website_id = $1
|
||||
and rev_id = $2
|
||||
and ${getBetweenDates('created_at', start_at, end_at)}
|
||||
${getFilterQuery('event', filters, params)}
|
||||
group by x, t
|
||||
|
@ -12,10 +12,8 @@ export function getEvents(...args) {
|
||||
function relationalQuery(websites, start_at) {
|
||||
return prisma.client.event.findMany({
|
||||
where: {
|
||||
website: {
|
||||
websiteUuid: {
|
||||
in: websites,
|
||||
},
|
||||
websiteId: {
|
||||
in: websites,
|
||||
},
|
||||
createdAt: {
|
||||
gte: start_at,
|
||||
|
@ -2,6 +2,8 @@ import { EVENT_NAME_LENGTH, URL_LENGTH } from 'lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import kafka from 'lib/kafka';
|
||||
import prisma from 'lib/prisma';
|
||||
import { uuid } from 'lib/crypto';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function saveEvent(...args) {
|
||||
return runQuery({
|
||||
@ -10,48 +12,51 @@ export async function saveEvent(...args) {
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(
|
||||
{ websiteId },
|
||||
{ session: { id: sessionId }, eventUuid, url, eventName, eventData },
|
||||
) {
|
||||
const data = {
|
||||
async function relationalQuery(data) {
|
||||
const { websiteId, sessionId, url, eventName, eventData } = data;
|
||||
const eventId = uuid();
|
||||
|
||||
const params = {
|
||||
id: eventId,
|
||||
websiteId,
|
||||
sessionId,
|
||||
url: url?.substring(0, URL_LENGTH),
|
||||
eventName: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||
eventUuid,
|
||||
};
|
||||
|
||||
if (eventData) {
|
||||
data.eventData = {
|
||||
params.eventData = {
|
||||
create: {
|
||||
id: eventId,
|
||||
eventData: eventData,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return prisma.client.event.create({
|
||||
data,
|
||||
data: params,
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
{ websiteUuid: websiteId },
|
||||
{ session: { country, sessionUuid, ...sessionArgs }, eventUuid, url, eventName, eventData },
|
||||
) {
|
||||
async function clickhouseQuery(data) {
|
||||
const { websiteId, id: sessionId, url, eventName, eventData, country, ...args } = data;
|
||||
const { getDateFormat, sendMessage } = kafka;
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
|
||||
const params = {
|
||||
session_id: sessionUuid,
|
||||
event_id: eventUuid,
|
||||
website_id: websiteId,
|
||||
created_at: getDateFormat(new Date()),
|
||||
session_id: sessionId,
|
||||
event_id: uuid(),
|
||||
url: url?.substring(0, URL_LENGTH),
|
||||
event_name: eventName?.substring(0, EVENT_NAME_LENGTH),
|
||||
event_data: eventData ? JSON.stringify(eventData) : null,
|
||||
...sessionArgs,
|
||||
rev_id: website?.revId || 0,
|
||||
created_at: getDateFormat(new Date()),
|
||||
country: country ? country : null,
|
||||
...args,
|
||||
};
|
||||
|
||||
await sendMessage(params, 'event');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function getPageviewMetrics(...args) {
|
||||
return runQuery({
|
||||
@ -24,7 +25,7 @@ async function relationalQuery(websiteId, { startDate, endDate, column, table, f
|
||||
from ${table}
|
||||
${` join website on ${table}.website_id = website.website_id`}
|
||||
${joinSession}
|
||||
where website.website_uuid='${websiteId}'
|
||||
where website.website_id='${websiteId}'
|
||||
and ${table}.created_at between $1 and $2
|
||||
${pageviewQuery}
|
||||
${joinSession && sessionQuery}
|
||||
@ -37,13 +38,15 @@ async function relationalQuery(websiteId, { startDate, endDate, column, table, f
|
||||
|
||||
async function clickhouseQuery(websiteId, { startDate, endDate, column, filters = {} }) {
|
||||
const { rawQuery, parseFilters, getBetweenDates } = clickhouse;
|
||||
const params = [websiteId];
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const params = [websiteId, website?.revId || 0];
|
||||
const { pageviewQuery, sessionQuery, eventQuery } = parseFilters(column, filters, params);
|
||||
|
||||
return rawQuery(
|
||||
`select ${column} x, count(*) y
|
||||
from event
|
||||
where website_id= $1
|
||||
where website_id = $1
|
||||
and rev_id = $2
|
||||
${column !== 'event_name' ? `and event_name = ''` : `and event_name != ''`}
|
||||
and ${getBetweenDates('created_at', startDate, endDate)}
|
||||
${pageviewQuery}
|
||||
|
@ -24,7 +24,7 @@ async function relationalQuery(websiteId, start_at, end_at, column, table, filte
|
||||
from ${table}
|
||||
${` join website on ${table}.website_id = website.website_id`}
|
||||
${joinSession}
|
||||
where website.website_uuid='${websiteId}'
|
||||
where website.website_id='${websiteId}'
|
||||
and ${table}.created_at between $1 and $2
|
||||
and ${table}.url like '%?%'
|
||||
${pageviewQuery}
|
||||
|
@ -1,6 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function getPageviewStats(...args) {
|
||||
return runQuery({
|
||||
@ -37,7 +38,7 @@ async function relationalQuery(
|
||||
join website
|
||||
on pageview.website_id = website.website_id
|
||||
${joinSession}
|
||||
where website.website_uuid='${websiteId}'
|
||||
where website.website_id='${websiteId}'
|
||||
and pageview.created_at between $1 and $2
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
@ -51,7 +52,8 @@ async function clickhouseQuery(
|
||||
{ start_at, end_at, timezone = 'UTC', unit = 'day', count = '*', filters = {} },
|
||||
) {
|
||||
const { parseFilters, rawQuery, getDateStringQuery, getDateQuery, getBetweenDates } = clickhouse;
|
||||
const params = [websiteId];
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const params = [websiteId, website?.revId || 0];
|
||||
const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params);
|
||||
|
||||
return rawQuery(
|
||||
@ -64,7 +66,8 @@ async function clickhouseQuery(
|
||||
count(${count !== '*' ? 'distinct session_id' : count}) y
|
||||
from event
|
||||
where event_name = ''
|
||||
and website_id= $1
|
||||
and website_id = $1
|
||||
and rev_id = $2
|
||||
and ${getBetweenDates('created_at', start_at, end_at)}
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
|
@ -12,10 +12,8 @@ export async function getPageviews(...args) {
|
||||
async function relationalQuery(websites, start_at) {
|
||||
return prisma.client.pageview.findMany({
|
||||
where: {
|
||||
website: {
|
||||
websiteUuid: {
|
||||
in: websites,
|
||||
},
|
||||
websiteId: {
|
||||
in: websites,
|
||||
},
|
||||
createdAt: {
|
||||
gte: start_at,
|
||||
@ -35,11 +33,11 @@ async function clickhouseQuery(websites, start_at) {
|
||||
url
|
||||
from event
|
||||
where event_name = ''
|
||||
and ${
|
||||
websites && websites.length > 0
|
||||
? `website_id in (${getCommaSeparatedStringFormat(websites)})`
|
||||
: '0 = 0'
|
||||
}
|
||||
and ${
|
||||
websites && websites.length > 0
|
||||
? `website_id in (${getCommaSeparatedStringFormat(websites)})`
|
||||
: '0 = 0'
|
||||
}
|
||||
and created_at >= ${clickhouse.getDateFormat(start_at)}`,
|
||||
);
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ import { URL_LENGTH } from 'lib/constants';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import kafka from 'lib/kafka';
|
||||
import prisma from 'lib/prisma';
|
||||
import cache from 'lib/cache';
|
||||
import { uuid } from 'lib/crypto';
|
||||
|
||||
export async function savePageView(...args) {
|
||||
return runQuery({
|
||||
@ -10,9 +12,11 @@ export async function savePageView(...args) {
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery({ websiteId }, { session: { id: sessionId }, url, referrer }) {
|
||||
async function relationalQuery(data) {
|
||||
const { websiteId, sessionId, url, referrer } = data;
|
||||
return prisma.client.pageview.create({
|
||||
data: {
|
||||
id: uuid(),
|
||||
websiteId,
|
||||
sessionId,
|
||||
url: url?.substring(0, URL_LENGTH),
|
||||
@ -21,20 +25,23 @@ async function relationalQuery({ websiteId }, { session: { id: sessionId }, url,
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
{ websiteUuid: websiteId },
|
||||
{ session: { country, sessionUuid, ...sessionArgs }, url, referrer },
|
||||
) {
|
||||
async function clickhouseQuery(data) {
|
||||
const { websiteId, id: sessionId, url, referrer, country, ...args } = data;
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const { getDateFormat, sendMessage } = kafka;
|
||||
const params = {
|
||||
session_uuid: sessionUuid,
|
||||
|
||||
const msg = {
|
||||
session_id: sessionId,
|
||||
website_id: websiteId,
|
||||
created_at: getDateFormat(new Date()),
|
||||
url: url?.substring(0, URL_LENGTH),
|
||||
referrer: referrer?.substring(0, URL_LENGTH),
|
||||
...sessionArgs,
|
||||
rev_id: website?.revId || 0,
|
||||
created_at: getDateFormat(new Date()),
|
||||
country: country ? country : null,
|
||||
...args,
|
||||
};
|
||||
|
||||
await sendMessage(params, 'event');
|
||||
await sendMessage(msg, 'event');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
@ -1,65 +1,45 @@
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import kafka from 'lib/kafka';
|
||||
import prisma from 'lib/prisma';
|
||||
import redis from 'lib/redis';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function createSession(...args) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
}).then(async data => {
|
||||
if (cache.enabled) {
|
||||
await cache.storeSession(data);
|
||||
}
|
||||
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(websiteId, data) {
|
||||
return prisma.client.session
|
||||
.create({
|
||||
data: {
|
||||
websiteId,
|
||||
...data,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
sessionUuid: true,
|
||||
hostname: true,
|
||||
browser: true,
|
||||
os: true,
|
||||
screen: true,
|
||||
language: true,
|
||||
country: true,
|
||||
device: true,
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (redis.enabled && res) {
|
||||
await redis.set(`session:${res.sessionUuid}`, 1);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
async function relationalQuery(data) {
|
||||
return prisma.client.session.create({ data });
|
||||
}
|
||||
|
||||
async function clickhouseQuery(
|
||||
websiteId,
|
||||
{ sessionUuid, hostname, browser, os, screen, language, country, device },
|
||||
) {
|
||||
async function clickhouseQuery(data) {
|
||||
const { id, websiteId, hostname, browser, os, device, screen, language, country } = data;
|
||||
const { getDateFormat, sendMessage } = kafka;
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
|
||||
const params = {
|
||||
session_uuid: sessionUuid,
|
||||
const msg = {
|
||||
session_id: id,
|
||||
website_id: websiteId,
|
||||
created_at: getDateFormat(new Date()),
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country: country ? country : null,
|
||||
country,
|
||||
rev_id: website?.revId || 0,
|
||||
created_at: getDateFormat(new Date()),
|
||||
};
|
||||
|
||||
await sendMessage(params, 'event');
|
||||
await sendMessage(msg, 'event');
|
||||
|
||||
if (redis.enabled) {
|
||||
await redis.set(`session:${sessionUuid}`, 1);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
39
queries/analytics/session/getSession.js
Normal file
39
queries/analytics/session/getSession.js
Normal file
@ -0,0 +1,39 @@
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import prisma from 'lib/prisma';
|
||||
|
||||
export async function getSession(...args) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(where) {
|
||||
return prisma.client.session.findUnique({
|
||||
where,
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery({ id: sessionId }) {
|
||||
const { rawQuery, findFirst } = clickhouse;
|
||||
const params = [sessionId];
|
||||
|
||||
return rawQuery(
|
||||
`select
|
||||
session_id,
|
||||
website_id,
|
||||
created_at,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country
|
||||
from event
|
||||
where session_id = $1
|
||||
limit 1`,
|
||||
params,
|
||||
).then(result => findFirst(result));
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
|
||||
import prisma from 'lib/prisma';
|
||||
import redis from 'lib/redis';
|
||||
|
||||
export async function getSessionByUuid(...args) {
|
||||
return runQuery({
|
||||
[PRISMA]: () => relationalQuery(...args),
|
||||
[CLICKHOUSE]: () => clickhouseQuery(...args),
|
||||
});
|
||||
}
|
||||
|
||||
async function relationalQuery(sessionUuid) {
|
||||
return prisma.client.session
|
||||
.findUnique({
|
||||
where: {
|
||||
sessionUuid,
|
||||
},
|
||||
})
|
||||
.then(async res => {
|
||||
if (redis.enabled && res) {
|
||||
await redis.set(`session:${res.sessionUuid}`, 1);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
async function clickhouseQuery(sessionUuid) {
|
||||
const { rawQuery, findFirst } = clickhouse;
|
||||
const params = [sessionUuid];
|
||||
|
||||
return rawQuery(
|
||||
`select distinct
|
||||
session_id,
|
||||
website_id,
|
||||
created_at,
|
||||
hostname,
|
||||
browser,
|
||||
os,
|
||||
device,
|
||||
screen,
|
||||
language,
|
||||
country
|
||||
from event
|
||||
where session_id = $1`,
|
||||
params,
|
||||
)
|
||||
.then(result => findFirst(result))
|
||||
.then(async res => {
|
||||
if (redis.enabled && res) {
|
||||
await redis.set(`session:${res.session_uuid}`, 1);
|
||||
}
|
||||
|
||||
return res;
|
||||
});
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function getSessionMetrics(...args) {
|
||||
return runQuery({
|
||||
@ -23,7 +24,7 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters =
|
||||
join website
|
||||
on pageview.website_id = website.website_id
|
||||
${joinSession}
|
||||
where website.website_uuid='${websiteId}'
|
||||
where website.website_id='${websiteId}'
|
||||
and pageview.created_at between $1 and $2
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
@ -36,13 +37,15 @@ async function relationalQuery(websiteId, { startDate, endDate, field, filters =
|
||||
|
||||
async function clickhouseQuery(websiteId, { startDate, endDate, field, filters = {} }) {
|
||||
const { parseFilters, getBetweenDates, rawQuery } = clickhouse;
|
||||
const params = [websiteId];
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const params = [websiteId, website?.revId || 0];
|
||||
const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params);
|
||||
|
||||
return rawQuery(
|
||||
`select ${field} x, count(*) y
|
||||
from event as x
|
||||
where website_id=$1
|
||||
where website_id = $1
|
||||
and rev_id = $2
|
||||
and event_name = ''
|
||||
and ${getBetweenDates('created_at', startDate, endDate)}
|
||||
${pageviewQuery}
|
||||
|
@ -14,10 +14,8 @@ async function relationalQuery(websites, start_at) {
|
||||
where: {
|
||||
...(websites && websites.length > 0
|
||||
? {
|
||||
website: {
|
||||
websiteUuid: {
|
||||
in: websites,
|
||||
},
|
||||
websiteId: {
|
||||
in: websites,
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
|
@ -19,7 +19,7 @@ async function relationalQuery(websiteId) {
|
||||
from pageview
|
||||
join website
|
||||
on pageview.website_id = website.website_id
|
||||
where website.website_uuid = '${websiteId}'
|
||||
where website.website_id = '${websiteId}'
|
||||
and pageview.created_at >= $1`,
|
||||
params,
|
||||
);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import prisma from 'lib/prisma';
|
||||
import clickhouse from 'lib/clickhouse';
|
||||
import { runQuery, CLICKHOUSE, PRISMA } from 'lib/db';
|
||||
import cache from 'lib/cache';
|
||||
|
||||
export async function getWebsiteStats(...args) {
|
||||
return runQuery({
|
||||
@ -33,7 +34,7 @@ async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) {
|
||||
join website
|
||||
on pageview.website_id = website.website_id
|
||||
${joinSession}
|
||||
where website.website_uuid='${websiteId}'
|
||||
where website.website_id='${websiteId}'
|
||||
and pageview.created_at between $1 and $2
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
@ -45,7 +46,8 @@ async function relationalQuery(websiteId, { start_at, end_at, filters = {} }) {
|
||||
|
||||
async function clickhouseQuery(websiteId, { start_at, end_at, filters = {} }) {
|
||||
const { rawQuery, getDateQuery, getBetweenDates, parseFilters } = clickhouse;
|
||||
const params = [websiteId];
|
||||
const website = await cache.fetchWebsite(websiteId);
|
||||
const params = [websiteId, website?.revId || 0];
|
||||
const { pageviewQuery, sessionQuery } = parseFilters(null, filters, params);
|
||||
|
||||
return rawQuery(
|
||||
@ -63,6 +65,7 @@ async function clickhouseQuery(websiteId, { start_at, end_at, filters = {} }) {
|
||||
from event
|
||||
where event_name = ''
|
||||
and website_id = $1
|
||||
and rev_id = $2
|
||||
and ${getBetweenDates('created_at', start_at, end_at)}
|
||||
${pageviewQuery}
|
||||
${sessionQuery}
|
||||
|
@ -1,8 +1,8 @@
|
||||
export * from './admin/account/createAccount';
|
||||
export * from './admin/account/deleteAccount';
|
||||
export * from './admin/account/getAccount';
|
||||
export * from './admin/account/getAccounts';
|
||||
export * from './admin/account/updateAccount';
|
||||
export * from './admin/user/createUser';
|
||||
export * from './admin/user/deleteUser';
|
||||
export * from './admin/user/getUser';
|
||||
export * from './admin/user/getUsers';
|
||||
export * from './admin/user/updateUser';
|
||||
export * from './admin/website/createWebsite';
|
||||
export * from './admin/website/deleteWebsite';
|
||||
export * from './admin/website/getAllWebsites';
|
||||
@ -20,7 +20,7 @@ export * from './analytics/pageview/getPageviews';
|
||||
export * from './analytics/pageview/getPageviewStats';
|
||||
export * from './analytics/pageview/savePageView';
|
||||
export * from './analytics/session/createSession';
|
||||
export * from './analytics/session/getSessionByUuid';
|
||||
export * from './analytics/session/getSession';
|
||||
export * from './analytics/session/getSessionMetrics';
|
||||
export * from './analytics/session/getSessions';
|
||||
export * from './analytics/stats/getActiveVisitors';
|
||||
|
@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-console */
|
||||
require('dotenv').config();
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
@ -13,9 +13,9 @@ const runQuery = async query => {
|
||||
});
|
||||
};
|
||||
|
||||
const updateAccountByUsername = (username, data) => {
|
||||
const updateUserByUsername = (username, data) => {
|
||||
return runQuery(
|
||||
prisma.account.update({
|
||||
prisma.user.update({
|
||||
where: {
|
||||
username,
|
||||
},
|
||||
@ -26,7 +26,7 @@ const updateAccountByUsername = (username, data) => {
|
||||
|
||||
const changePassword = async (username, newPassword) => {
|
||||
const password = hashPassword(newPassword);
|
||||
return updateAccountByUsername(username, { password });
|
||||
return updateUserByUsername(username, { password });
|
||||
};
|
||||
|
||||
const getUsernameAndPassword = async () => {
|
||||
@ -40,7 +40,7 @@ const getUsernameAndPassword = async () => {
|
||||
questions.push({
|
||||
type: 'text',
|
||||
name: 'username',
|
||||
message: 'Enter account to change password',
|
||||
message: 'Enter user to change password',
|
||||
});
|
||||
}
|
||||
if (!password) {
|
||||
@ -84,7 +84,7 @@ const getUsernameAndPassword = async () => {
|
||||
console.log('Password changed for user', chalk.greenBright(username));
|
||||
} catch (error) {
|
||||
if (error.meta.cause.includes('Record to update not found')) {
|
||||
console.log('Account not found:', chalk.redBright(username));
|
||||
console.log('User not found:', chalk.redBright(username));
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user