This commit is contained in:
Brian Cao 2022-10-31 23:42:37 -07:00
parent 246e4e5f4f
commit 17041efaae
73 changed files with 491 additions and 874 deletions

View File

@ -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.is/umami-software/umami:postgresql-latest
```
Or with MySQL support:
```bash
docker pull docker.umami.is/umami-software/umami:mysql-latest
```

View File

@ -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();

View File

@ -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

View File

@ -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();

View File

@ -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">

View File

@ -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,
})),
);

View File

@ -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}

View File

@ -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>

View File

@ -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>

View File

@ -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>
</>

View File

@ -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}

View File

@ -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}
/>

View File

@ -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}
/>

View File

@ -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);

View File

@ -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";

View File

@ -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;

View File

@ -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");

View File

@ -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"

View File

@ -7,55 +7,54 @@ 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[]
}
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])
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)
eventName String @map("event_name") @db.VarChar(50)
eventData eventData?
@@index([createdAt])
@@index([sessionId])
@@index([websiteId])
@@index([eventUuid])
}
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])
id String @id @unique @map("event_data_id") @db.Uuid
eventId String @unique @map("event_id") @db.Uuid
eventData Json @map("event_data")
event event @relation(fields: [eventId], references: [id])
@@map("event_data")
}
model pageview {
id Int @id @default(autoincrement()) @map("view_id")
websiteId Int @map("website_id")
sessionId Int @map("session_id")
id String @id @unique @map("view_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])
@@index([createdAt])
@@index([sessionId])
@ -65,39 +64,154 @@ model pageview {
}
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[]
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([websiteId])
@@index([sessionUuid])
}
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[]
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)
createdAt DateTime? @default(now()) @map("created_at") @db.Timestamptz(6)
user user @relation(fields: [userId], references: [id])
teamWebsite teamWebsite[]
userWebsite userWebsite[]
@@index([userId])
@@index([websiteUuid])
}
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")
groupRoles groupRole[]
groupUsers groupUser[]
}
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")
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")
}
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[]
}
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[]
}
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)
isDeleted Boolean @default(false) @map("is_deleted")
website website @relation(fields: [websiteId], references: [id])
user user @relation(fields: [userId], references: [id])
@@map("user_website")
}

5
interface/auth.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
export interface Auth {
id: number;
email?: string;
teams?: string[];
}

22
interface/base.d.ts vendored Normal file
View 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
View 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;
}

View File

@ -1,5 +1,5 @@
import { parseSecureToken, parseToken } from 'next-basics';
import { getAccount, getWebsite } from 'queries';
import { getUser, getWebsite } from 'queries';
import { SHARE_TOKEN_HEADER, TYPE_ACCOUNT, TYPE_WEBSITE } from 'lib/constants';
import { secret } from 'lib/crypto';
@ -50,13 +50,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 });
const user = await getUser({ id });
return account && account.accountUuid === id;
return user && user.id === id;
}
}

View File

@ -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_ACCOUNT = 'user';
export const THEME_COLORS = {
light: {

View File

@ -1,11 +1,8 @@
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;
@ -32,30 +29,6 @@ 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();
@ -76,4 +49,4 @@ async function connect() {
return redis;
}
export default { enabled, client: redis, log, connect, get, set, stageData };
export default { enabled, client: redis, log, connect, get, set };

View File

@ -4,7 +4,7 @@ import { secret, uuid } from 'lib/crypto';
import redis, { DELETED } from 'lib/redis';
import clickhouse from 'lib/clickhouse';
import { getClientInfo, getJsonBody } from 'lib/request';
import { createSession, getSessionByUuid, getWebsite } from 'queries';
import { createSession, getSession as getSessionPrisma, getWebsite } from 'queries';
export async function getSession(req) {
const { payload } = getJsonBody(req);
@ -23,51 +23,51 @@ export async function getSession(req) {
}
}
const { website: websiteUuid, hostname, screen, language } = payload;
const { website: websiteId, hostname, screen, language } = payload;
if (!validate(websiteUuid)) {
if (!validate(websiteId)) {
return null;
}
let websiteId = null;
let isValidWebsite = null;
// Check if website exists
if (redis.enabled) {
websiteId = Number(await redis.get(`website:${websiteUuid}`));
isValidWebsite = await redis.get(`website:${websiteId}`);
}
// Check database if does not exists in Redis
if (!websiteId) {
const website = await getWebsite({ websiteUuid });
websiteId = website ? website.id : null;
if (!isValidWebsite) {
const website = await getWebsite({ id: websiteId });
isValidWebsite = !!website;
}
if (!websiteId || websiteId === DELETED) {
throw new Error(`Website not found: ${websiteUuid}`);
if (!isValidWebsite || isValidWebsite === DELETED) {
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 isValidSession = null;
let session = null;
if (!clickhouse.enabled) {
// Check if session exists
if (redis.enabled) {
sessionId = Number(await redis.get(`session:${sessionUuid}`));
isValidSession = await redis.get(`session:${sessionId}`);
}
// Check database if does not exists in Redis
if (!sessionId) {
session = await getSessionByUuid(sessionUuid);
sessionId = session ? session.id : null;
if (!isValidSession) {
session = await getSessionPrisma({ id: sessionId });
isValidSession = !!session;
}
if (!sessionId) {
if (!isValidSession) {
try {
session = await createSession(websiteId, {
sessionUuid,
id: sessionId,
hostname,
browser,
os,
@ -84,8 +84,7 @@ export async function getSession(req) {
}
} else {
session = {
sessionId,
sessionUuid,
id: sessionId,
hostname,
browser,
os,
@ -97,10 +96,7 @@ export async function getSession(req) {
}
return {
website: {
websiteId,
websiteUuid,
},
websiteId,
session,
};
}

5
next-env.d.ts vendored Normal file
View 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.

View File

@ -1,5 +1,5 @@
import { ok, unauthorized, badRequest, checkPassword, createSecureToken } from 'next-basics';
import { getAccount } from 'queries';
import { getUser } from 'queries';
import { secret } from 'lib/crypto';
export default async (req, res) => {
@ -9,12 +9,11 @@ export default async (req, res) => {
return badRequest(res);
}
const account = await getAccount({ username });
const user = await getUser({ 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());
if (user && checkPassword(password, user.password)) {
const { id: userId, username, isAdmin } = user;
const token = createSecureToken({ userId, username, isAdmin }, secret());
return ok(res, { token, user });
}

View File

@ -58,7 +58,7 @@ export default async (req, res) => {
await useSession(req, res);
const { website, session } = req.session;
const { websiteId, session } = req.session;
const { type, payload } = getJsonBody(req);
@ -68,14 +68,12 @@ export default async (req, res) => {
url = url.replace(/\/$/, '');
}
const eventUuid = uuid();
if (type === 'pageview') {
await savePageView(website, { session, url, referrer });
await savePageView(websiteId, { pageViewId: uuid(), session, url, referrer });
} else if (type === 'event') {
await saveEvent(website, {
await saveEvent(websiteId, {
eventId: uuid(),
session,
eventUuid,
url,
eventName,
eventData,
@ -86,7 +84,7 @@ export default async (req, res) => {
const token = createToken(
{
website,
websiteId,
session,
},
secret(),

View File

@ -11,7 +11,7 @@ export default async (req, res) => {
const { userId } = req.auth;
const websites = await getUserWebsites({ userId });
const ids = websites.map(({ websiteUuid }) => websiteUuid);
const ids = websites.map(({ id }) => id);
const token = createToken({ websites: ids }, secret());
const data = await getRealtimeData(ids, subMinutes(new Date(), 30));

View File

@ -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 });

View File

@ -1,5 +1,5 @@
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) => {
@ -13,9 +13,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 +25,7 @@ export default async (req, res) => {
return unauthorized(res);
}
const account = await getAccount({ id: +id });
const user = await getUser({ id });
const data = {};
@ -39,15 +39,15 @@ 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);
}
@ -57,7 +57,7 @@ export default async (req, res) => {
return unauthorized(res);
}
await deleteAccount(userId);
await deleteUser(id);
return ok(res);
}

View File

@ -1,4 +1,4 @@
import { getAccount, updateAccount } from 'queries';
import { getUser, updateUser } from 'queries';
import { useAuth } from 'lib/middleware';
import {
badRequest,
@ -15,22 +15,22 @@ 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))) {
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);
}

View File

@ -1,7 +1,7 @@
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);
@ -13,24 +13,24 @@ export default async (req, 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 } = 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: uuid(),
username,
password: hashPassword(password),
accountUuid: account_uuid || uuid(),
});
return ok(res, created);

View File

@ -1,39 +1,39 @@
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 { deleteWebsite, getUser, 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 } = 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 });
return ok(res, website);
}
if (req.method === 'POST') {
const { name, domain, owner, enableShareUrl, shareId } = req.body;
const { accountUuid } = req.auth;
let account;
const { userId } = req.auth;
let user;
if (accountUuid) {
account = await getAccount({ accountUuid });
if (userId) {
user = await getUser({ id: userId });
if (!account) {
return serverError(res, 'Account does not exist.');
if (!user) {
return serverError(res, 'User does not exist.');
}
}
const website = await getWebsite({ websiteUuid });
const website = await getWebsite({ id });
const newShareId = enableShareUrl ? website.shareId || getRandomChars(8) : null;
@ -43,9 +43,9 @@ export default async (req, res) => {
name,
domain,
shareId: shareId ? shareId : newShareId,
userId: account ? account.id : +owner || undefined,
userId: +owner || user.id,
},
{ websiteUuid },
{ id },
);
} catch (e) {
if (e.message.includes('Unique constraint') && e.message.includes('share_id')) {
@ -61,7 +61,7 @@ export default async (req, res) => {
return unauthorized(res);
}
await deleteWebsite(websiteUuid);
await deleteWebsite(id);
return ok(res);
}

View File

@ -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);

View File

@ -1,4 +1,4 @@
import { createWebsite, getAccount, getAllWebsites, getUserWebsites } from 'queries';
import { createWebsite, getUser, getAllWebsites, getUserWebsites } from 'queries';
import { ok, methodNotAllowed, unauthorized, getRandomChars } from 'next-basics';
import { useAuth } from 'lib/middleware';
import { uuid } from 'lib/crypto';
@ -8,14 +8,14 @@ export default async (req, res) => {
const { user_id, include_all } = req.query;
const { userId: currentUserId, isAdmin } = req.auth;
const accountUuid = user_id || req.auth.accountUuid;
let account;
const id = user_id || currentUserId;
let user;
if (accountUuid) {
account = await getAccount({ accountUuid });
if (id) {
user = await getUser({ id });
}
const userId = account ? account.id : user_id;
const userId = user ? user.id : user_id;
if (req.method === 'GET') {
if (userId && userId !== currentUserId && !isAdmin) {
@ -23,9 +23,7 @@ export default async (req, res) => {
}
const websites =
isAdmin && include_all
? await getAllWebsites()
: await getUserWebsites({ userId: account?.id });
isAdmin && include_all ? await getAllWebsites() : await getUserWebsites({ userId });
return ok(res, websites);
}
@ -33,15 +31,14 @@ export default async (req, res) => {
if (req.method === 'POST') {
const { name, domain, owner, enableShareUrl } = req.body;
const website_owner = account ? account.id : +owner;
const website_owner = user ? userId : +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(website_owner, { id: uuid(), name, domain, shareId });
return ok(res, website);
}

View File

@ -1,7 +0,0 @@
import prisma from 'lib/prisma';
export async function createAccount(data) {
return prisma.client.account.create({
data,
});
}

View File

@ -1,7 +0,0 @@
import prisma from 'lib/prisma';
export async function getAccount(where) {
return prisma.client.account.findUnique({
where,
});
}

View File

@ -1,8 +0,0 @@
import prisma from 'lib/prisma';
export async function updateAccount(data, where) {
return prisma.client.account.update({
where,
data,
});
}

View File

@ -0,0 +1,7 @@
import prisma from 'lib/prisma';
export async function createUser(data) {
return prisma.client.user.create({
data,
});
}

View File

@ -1,18 +1,17 @@
import prisma from 'lib/prisma';
import redis, { DELETED } from 'lib/redis';
export async function deleteAccount(userId) {
export async function deleteUser(userId) {
const { client } = prisma;
const websites = await client.website.findMany({
where: { userId },
select: { websiteUuid: true },
});
let websiteUuids = [];
let websiteIds = [];
if (websites.length > 0) {
websiteUuids = websites.map(a => a.websiteUuid);
websiteIds = websites.map(a => a.id);
}
return client
@ -32,7 +31,7 @@ export async function deleteAccount(userId) {
client.website.deleteMany({
where: { userId },
}),
client.account.delete({
client.user.delete({
where: {
id: userId,
},
@ -40,8 +39,8 @@ export async function deleteAccount(userId) {
])
.then(async res => {
if (redis.enabled) {
for (let i = 0; i < websiteUuids.length; i++) {
await redis.set(`website:${websiteUuids[i]}`, DELETED);
for (let i = 0; i < websiteIds.length; i++) {
await redis.set(`website:${websiteIds[i]}`, DELETED);
}
}

View File

@ -0,0 +1,7 @@
import prisma from 'lib/prisma';
export async function getUser(where) {
return prisma.client.user.findUnique({
where,
});
}

View File

@ -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,
},
});
}

View File

@ -0,0 +1,8 @@
import prisma from 'lib/prisma';
export async function updateUser(data, where) {
return prisma.client.user.update({
where,
data,
});
}

View File

@ -5,7 +5,7 @@ export async function createWebsite(userId, data) {
return prisma.client.website
.create({
data: {
account: {
user: {
connect: {
id: userId,
},
@ -15,7 +15,7 @@ export async function createWebsite(userId, data) {
})
.then(async res => {
if (redis.enabled && res) {
await redis.set(`website:${res.websiteUuid}`, res.id);
await redis.set(`website:${res.id}`, 1);
}
return res;

View File

@ -1,28 +1,28 @@
import prisma from 'lib/prisma';
import redis, { DELETED } from 'lib/redis';
export async function deleteWebsite(websiteUuid) {
export async function deleteWebsite(id) {
const { client, transaction } = prisma;
return transaction([
client.pageview.deleteMany({
where: { session: { website: { websiteUuid } } },
where: { session: { website: { id } } },
}),
client.eventData.deleteMany({
where: { event: { session: { website: { websiteUuid } } } },
where: { event: { session: { website: { id } } } },
}),
client.event.deleteMany({
where: { session: { website: { websiteUuid } } },
where: { session: { website: { id } } },
}),
client.session.deleteMany({
where: { website: { websiteUuid } },
where: { website: { id } },
}),
client.website.delete({
where: { websiteUuid },
where: { id },
}),
]).then(async res => {
if (redis.enabled) {
await redis.set(`website:${websiteUuid}`, DELETED);
await redis.set(`website:${id}`, DELETED);
}
return res;

View File

@ -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 }));
}

View File

@ -8,7 +8,7 @@ export async function getWebsite(where) {
})
.then(async data => {
if (redis.enabled && data) {
await redis.set(`website:${data.websiteUuid}`, data.id);
await redis.set(`website:${data.id}`, 1);
}
return data;

View File

@ -1,20 +1,20 @@
import prisma from 'lib/prisma';
export async function resetWebsite(websiteId) {
export async function resetWebsite(id) {
const { client, transaction } = prisma;
return transaction([
client.pageview.deleteMany({
where: { session: { website: { websiteUuid: websiteId } } },
where: { session: { website: { id } } },
}),
client.eventData.deleteMany({
where: { event: { session: { website: { websiteUuid: websiteId } } } },
where: { event: { session: { website: { id } } } },
}),
client.event.deleteMany({
where: { session: { website: { websiteUuid: websiteId } } },
where: { session: { website: { id } } },
}),
client.session.deleteMany({
where: { website: { websiteUuid: websiteId } },
where: { website: { id } },
}),
]);
}

View File

@ -21,7 +21,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}` : ''}
${

View File

@ -28,7 +28,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

View File

@ -13,7 +13,7 @@ function relationalQuery(websites, start_at) {
return prisma.client.event.findMany({
where: {
website: {
websiteUuid: {
id: {
in: websites,
},
},

View File

@ -2,6 +2,7 @@ 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';
export async function saveEvent(...args) {
return runQuery({
@ -11,10 +12,11 @@ export async function saveEvent(...args) {
}
async function relationalQuery(
{ websiteId },
{ session: { id: sessionId }, eventUuid, url, eventName, eventData },
websiteId,
{ eventId, session: { id: sessionId }, eventUuid, url, eventName, eventData },
) {
const data = {
id: eventId,
websiteId,
sessionId,
url: url?.substring(0, URL_LENGTH),
@ -26,6 +28,7 @@ async function relationalQuery(
data.eventData = {
create: {
eventData: eventData,
id: uuid(),
},
};
}
@ -36,7 +39,7 @@ async function relationalQuery(
}
async function clickhouseQuery(
{ websiteUuid: websiteId },
websiteId,
{ session: { country, sessionUuid, ...sessionArgs }, eventUuid, url, eventName, eventData },
) {
const { getDateFormat, sendMessage } = kafka;

View File

@ -24,7 +24,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}

View File

@ -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}

View File

@ -37,7 +37,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}

View File

@ -13,7 +13,7 @@ async function relationalQuery(websites, start_at) {
return prisma.client.pageview.findMany({
where: {
website: {
websiteUuid: {
id: {
in: websites,
},
},

View File

@ -10,9 +10,13 @@ export async function savePageView(...args) {
});
}
async function relationalQuery({ websiteId }, { session: { id: sessionId }, url, referrer }) {
async function relationalQuery(
websiteId,
{ pageViewId, session: { id: sessionId }, url, referrer },
) {
return prisma.client.pageview.create({
data: {
id: pageViewId,
websiteId,
sessionId,
url: url?.substring(0, URL_LENGTH),
@ -22,12 +26,12 @@ async function relationalQuery({ websiteId }, { session: { id: sessionId }, url,
}
async function clickhouseQuery(
{ websiteUuid: websiteId },
{ session: { country, sessionUuid, ...sessionArgs }, url, referrer },
websiteId,
{ session: { country, id: sessionId, ...sessionArgs }, url, referrer },
) {
const { getDateFormat, sendMessage } = kafka;
const params = {
session_uuid: sessionUuid,
session_id: sessionId,
website_id: websiteId,
created_at: getDateFormat(new Date()),
url: url?.substring(0, URL_LENGTH),

View File

@ -19,7 +19,6 @@ async function relationalQuery(websiteId, data) {
},
select: {
id: true,
sessionUuid: true,
hostname: true,
browser: true,
os: true,
@ -31,7 +30,7 @@ async function relationalQuery(websiteId, data) {
})
.then(async res => {
if (redis.enabled && res) {
await redis.set(`session:${res.sessionUuid}`, 1);
await redis.set(`session:${res.id}`, 1);
}
return res;
@ -40,12 +39,12 @@ async function relationalQuery(websiteId, data) {
async function clickhouseQuery(
websiteId,
{ sessionUuid, hostname, browser, os, screen, language, country, device },
{ sessionId, hostname, browser, os, screen, language, country, device },
) {
const { getDateFormat, sendMessage } = kafka;
const params = {
session_uuid: sessionUuid,
sessionId,
website_id: websiteId,
created_at: getDateFormat(new Date()),
hostname,
@ -60,6 +59,6 @@ async function clickhouseQuery(
await sendMessage(params, 'event');
if (redis.enabled) {
await redis.set(`session:${sessionUuid}`, 1);
await redis.set(`session:${sessionId}`, 1);
}
}

View File

@ -3,19 +3,17 @@ import { CLICKHOUSE, PRISMA, runQuery } from 'lib/db';
import prisma from 'lib/prisma';
import redis from 'lib/redis';
export async function getSessionByUuid(...args) {
export async function getSession(...args) {
return runQuery({
[PRISMA]: () => relationalQuery(...args),
[CLICKHOUSE]: () => clickhouseQuery(...args),
});
}
async function relationalQuery(sessionUuid) {
async function relationalQuery(where) {
return prisma.client.session
.findUnique({
where: {
sessionUuid,
},
where,
})
.then(async res => {
if (redis.enabled && res) {
@ -49,7 +47,7 @@ async function clickhouseQuery(sessionUuid) {
.then(result => findFirst(result))
.then(async res => {
if (redis.enabled && res) {
await redis.set(`session:${res.session_uuid}`, 1);
await redis.set(`session:${res.id}`, 1);
}
return res;

View File

@ -23,7 +23,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}

View File

@ -15,7 +15,7 @@ async function relationalQuery(websites, start_at) {
...(websites && websites.length > 0
? {
website: {
websiteUuid: {
id: {
in: websites,
},
},

View File

@ -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,
);

View File

@ -33,7 +33,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}

View File

@ -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';

View File

@ -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;
}

View File

@ -40,7 +40,7 @@ async function checkConnection() {
async function checkTables() {
try {
await prisma.$queryRaw`select * from account limit 1`;
await prisma.$queryRaw`select * from user limit 1`;
success('Database tables found.');
} catch (e) {

View File

@ -1,102 +0,0 @@
-- CreateTable
CREATE TABLE `account` (
`user_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`username` VARCHAR(255) NOT NULL,
`password` VARCHAR(60) NOT NULL,
`is_admin` BOOLEAN NOT NULL DEFAULT false,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`updated_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
UNIQUE INDEX `username`(`username`),
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `event` (
`event_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`website_id` INTEGER UNSIGNED NOT NULL,
`session_id` INTEGER UNSIGNED NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`url` VARCHAR(500) NOT NULL,
`event_type` VARCHAR(50) NOT NULL,
`event_value` VARCHAR(50) NOT NULL,
INDEX `event_created_at_idx`(`created_at`),
INDEX `event_session_id_idx`(`session_id`),
INDEX `event_website_id_idx`(`website_id`),
PRIMARY KEY (`event_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `pageview` (
`view_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`website_id` INTEGER UNSIGNED NOT NULL,
`session_id` INTEGER UNSIGNED NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`url` VARCHAR(500) NOT NULL,
`referrer` VARCHAR(500) NULL,
INDEX `pageview_created_at_idx`(`created_at`),
INDEX `pageview_session_id_idx`(`session_id`),
INDEX `pageview_website_id_created_at_idx`(`website_id`, `created_at`),
INDEX `pageview_website_id_idx`(`website_id`),
INDEX `pageview_website_id_session_id_created_at_idx`(`website_id`, `session_id`, `created_at`),
PRIMARY KEY (`view_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `session` (
`session_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`session_uuid` VARCHAR(36) NOT NULL,
`website_id` INTEGER UNSIGNED NOT NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
`hostname` VARCHAR(100) NULL,
`browser` VARCHAR(20) NULL,
`os` VARCHAR(20) NULL,
`device` VARCHAR(20) NULL,
`screen` VARCHAR(11) NULL,
`language` VARCHAR(35) NULL,
`country` CHAR(2) NULL,
UNIQUE INDEX `session_uuid`(`session_uuid`),
INDEX `session_created_at_idx`(`created_at`),
INDEX `session_website_id_idx`(`website_id`),
PRIMARY KEY (`session_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- CreateTable
CREATE TABLE `website` (
`website_id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`website_uuid` VARCHAR(36) NOT NULL,
`user_id` INTEGER UNSIGNED NOT NULL,
`name` VARCHAR(100) NOT NULL,
`domain` VARCHAR(500) NULL,
`share_id` VARCHAR(64) NULL,
`created_at` TIMESTAMP(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
UNIQUE INDEX `website_uuid`(`website_uuid`),
UNIQUE INDEX `share_id`(`share_id`),
INDEX `website_user_id_idx`(`user_id`),
PRIMARY KEY (`website_id`)
) ENGINE=InnoDB DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
-- AddForeignKey
ALTER TABLE `event` ADD CONSTRAINT `event_ibfk_2` FOREIGN KEY (`session_id`) REFERENCES `session`(`session_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE `event` ADD CONSTRAINT `event_ibfk_1` FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE `pageview` ADD CONSTRAINT `pageview_ibfk_2` FOREIGN KEY (`session_id`) REFERENCES `session`(`session_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE `pageview` ADD CONSTRAINT `pageview_ibfk_1` FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE `session` ADD CONSTRAINT `session_ibfk_1` FOREIGN KEY (`website_id`) REFERENCES `website`(`website_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- AddForeignKey
ALTER TABLE `website` ADD CONSTRAINT `website_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `account`(`user_id`) ON DELETE CASCADE ON UPDATE NO ACTION;
-- CreateAdminUser
INSERT INTO account (username, password, is_admin) values ('admin', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);

View File

@ -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);

View File

@ -92,21 +92,21 @@
.then(text => (cache = text));
};
const trackView = (url = currentUrl, referrer = currentRef, websiteUuid = website) =>
const trackView = (url = currentUrl, referrer = currentRef, websiteId = website) =>
collect(
'pageview',
assign(getPayload(), {
website: websiteUuid,
website: websiteId,
url,
referrer,
}),
);
const trackEvent = (eventName, eventData, url = currentUrl, websiteUuid = website) =>
const trackEvent = (eventName, eventData, url = currentUrl, websiteId = website) =>
collect(
'event',
assign(getPayload(), {
website: websiteUuid,
website: websiteId,
url,
event_name: eventName,
event_data: eventData,