diff --git a/components/common/RefreshButton.js b/components/common/RefreshButton.js
index 0b197bd2..be1a8a1d 100644
--- a/components/common/RefreshButton.js
+++ b/components/common/RefreshButton.js
@@ -11,7 +11,7 @@ import useDateRange from 'hooks/useDateRange';
function RefreshButton({ websiteId }) {
const [dateRange] = useDateRange(websiteId);
const [loading, setLoading] = useState(false);
- const selector = useCallback(state => state[`/website/${websiteId}/stats`], [websiteId]);
+ const selector = useCallback(state => state[`/websites/${websiteId}/stats`], [websiteId]);
const completed = useStore(selector);
function handleClick() {
diff --git a/components/forms/AccountEditForm.js b/components/forms/AccountEditForm.js
index 86825629..bedc3b82 100644
--- a/components/forms/AccountEditForm.js
+++ b/components/forms/AccountEditForm.js
@@ -34,7 +34,7 @@ export default function AccountEditForm({ values, onSave, onClose }) {
const handleSubmit = async values => {
const { user_id } = values;
- const { ok, data } = await post(user_id ? `/account/${user_id}` : '/account', values);
+ const { ok, data } = await post(user_id ? `/accounts/${user_id}` : '/accounts', values);
if (ok) {
onSave();
diff --git a/components/forms/ChangePasswordForm.js b/components/forms/ChangePasswordForm.js
index 29f34521..4870d523 100644
--- a/components/forms/ChangePasswordForm.js
+++ b/components/forms/ChangePasswordForm.js
@@ -9,6 +9,7 @@ import FormLayout, {
FormRow,
} from 'components/layout/FormLayout';
import useApi from 'hooks/useApi';
+import useUser from '../../hooks/useUser';
const initialValues = {
current_password: '',
@@ -39,9 +40,10 @@ const validate = ({ current_password, new_password, confirm_password }) => {
export default function ChangePasswordForm({ values, onSave, onClose }) {
const { post } = useApi();
const [message, setMessage] = useState();
+ const { user } = useUser();
const handleSubmit = async values => {
- const { ok, data } = await post('/account/password', values);
+ const { ok, data } = await post(`/accounts/${user.user_id}/password`, values);
if (ok) {
onSave();
diff --git a/components/forms/WebsiteEditForm.js b/components/forms/WebsiteEditForm.js
index 80a6dfc0..21daeeaf 100644
--- a/components/forms/WebsiteEditForm.js
+++ b/components/forms/WebsiteEditForm.js
@@ -38,7 +38,6 @@ const validate = ({ name, domain }) => {
};
const OwnerDropDown = ({ user, accounts }) => {
- console.info(styles);
const { setFieldValue, values } = useFormikContext();
useEffect(() => {
@@ -79,7 +78,8 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
const [message, setMessage] = useState();
const handleSubmit = async values => {
- const { ok, data } = await post('/website', values);
+ const { website_id } = values;
+ const { ok, data } = await post(website_id ? `/websites/${website_id}` : '/websites', values);
if (ok) {
onSave();
@@ -137,6 +137,7 @@ export default function WebsiteEditForm({ values, onSave, onClose }) {
defaultMessage="Enable share URL"
/>
}
+ value={null}
/>
)}
diff --git a/components/metrics/ActiveUsers.js b/components/metrics/ActiveUsers.js
index 653fc783..0ab266d3 100644
--- a/components/metrics/ActiveUsers.js
+++ b/components/metrics/ActiveUsers.js
@@ -6,7 +6,7 @@ import Dot from 'components/common/Dot';
import styles from './ActiveUsers.module.css';
export default function ActiveUsers({ websiteId, className, value, interval = 60000 }) {
- const url = websiteId ? `/website/${websiteId}/active` : null;
+ const url = websiteId ? `/websites/${websiteId}/active` : null;
const { data } = useFetch(url, {
interval,
});
diff --git a/components/metrics/EventsChart.js b/components/metrics/EventsChart.js
index 50dc940d..a8735b5d 100644
--- a/components/metrics/EventsChart.js
+++ b/components/metrics/EventsChart.js
@@ -16,7 +16,7 @@ export default function EventsChart({ websiteId, className, token }) {
} = usePageQuery();
const { data, loading } = useFetch(
- `/website/${websiteId}/events`,
+ `/websites/${websiteId}/events`,
{
params: {
start_at: +startDate,
diff --git a/components/metrics/MetricsBar.js b/components/metrics/MetricsBar.js
index 21928b61..aa260c8b 100644
--- a/components/metrics/MetricsBar.js
+++ b/components/metrics/MetricsBar.js
@@ -19,7 +19,7 @@ export default function MetricsBar({ websiteId, className }) {
} = usePageQuery();
const { data, error, loading } = useFetch(
- `/website/${websiteId}/stats`,
+ `/websites/${websiteId}/stats`,
{
params: {
start_at: +startDate,
diff --git a/components/metrics/MetricsTable.js b/components/metrics/MetricsTable.js
index fc62e518..9079d190 100644
--- a/components/metrics/MetricsTable.js
+++ b/components/metrics/MetricsTable.js
@@ -38,7 +38,7 @@ export default function MetricsTable({
const { formatMessage } = useIntl();
const { data, loading, error } = useFetch(
- `/website/${websiteId}/metrics`,
+ `/websites/${websiteId}/metrics`,
{
params: {
type,
diff --git a/components/metrics/WebsiteChart.js b/components/metrics/WebsiteChart.js
index fd74538f..48b97f26 100644
--- a/components/metrics/WebsiteChart.js
+++ b/components/metrics/WebsiteChart.js
@@ -35,7 +35,7 @@ export default function WebsiteChart({
const { get } = useApi();
const { data, loading, error } = useFetch(
- `/website/${websiteId}/pageviews`,
+ `/websites/${websiteId}/pageviews`,
{
params: {
start_at: +startDate,
@@ -70,7 +70,7 @@ export default function WebsiteChart({
async function handleDateChange(value) {
if (value === 'all') {
- const { data, ok } = await get(`/website/${websiteId}`);
+ const { data, ok } = await get(`/websites/${websiteId}`);
if (ok) {
setDateRange({ value, ...getDateRangeValues(new Date(data.created_at), Date.now()) });
}
diff --git a/components/metrics/WebsiteHeader.js b/components/metrics/WebsiteHeader.js
index 15f6d0ff..1a6bdf15 100644
--- a/components/metrics/WebsiteHeader.js
+++ b/components/metrics/WebsiteHeader.js
@@ -17,8 +17,8 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
{title}
@@ -41,8 +41,8 @@ export default function WebsiteHeader({ websiteId, title, domain, showLink = fal
{showLink && (
}
size="small"
diff --git a/components/pages/WebsiteDetails.js b/components/pages/WebsiteDetails.js
index 78c5f752..3fc234a4 100644
--- a/components/pages/WebsiteDetails.js
+++ b/components/pages/WebsiteDetails.js
@@ -52,7 +52,7 @@ const views = {
};
export default function WebsiteDetails({ websiteId }) {
- const { data } = useFetch(`/website/${websiteId}`);
+ const { data } = useFetch(`/websites/${websiteId}`);
const [chartLoaded, setChartLoaded] = useState(false);
const [countryData, setCountryData] = useState();
const [eventsData, setEventsData] = useState();
diff --git a/components/settings/WebsiteSettings.js b/components/settings/WebsiteSettings.js
index 451be47f..49bd005f 100644
--- a/components/settings/WebsiteSettings.js
+++ b/components/settings/WebsiteSettings.js
@@ -86,8 +86,8 @@ export default function WebsiteSettings() {
const DetailsLink = ({ website_id, name, domain }) => (
{name}
diff --git a/db/mysql/migrations/04_account_uuid/migration.sql b/db/mysql/migrations/04_account_uuid/migration.sql
new file mode 100644
index 00000000..7b7b5dea
--- /dev/null
+++ b/db/mysql/migrations/04_account_uuid/migration.sql
@@ -0,0 +1,11 @@
+-- AlterTable
+ALTER TABLE `account` ADD COLUMN `account_uuid` VARCHAR(36);
+
+-- Backfill UUID
+UPDATE `account` SET account_uuid=(SELECT uuid());
+
+-- AlterTable
+ALTER TABLE `account` MODIFY `account_uuid` VARCHAR(36) NOT NULL;
+
+-- CreateIndex
+CREATE UNIQUE INDEX `account_account_uuid_key` ON `account`(`account_uuid`);
diff --git a/db/mysql/schema.prisma b/db/mysql/schema.prisma
index 9ad2620c..bfc2c20b 100644
--- a/db/mysql/schema.prisma
+++ b/db/mysql/schema.prisma
@@ -8,13 +8,14 @@ datasource db {
}
model account {
- user_id Int @id @default(autoincrement()) @db.UnsignedInt
- username String @unique() @db.VarChar(255)
- password String @db.VarChar(60)
- is_admin Boolean @default(false)
- created_at DateTime? @default(now()) @db.Timestamp(0)
- updated_at DateTime? @default(now()) @db.Timestamp(0)
- website website[]
+ user_id Int @id @default(autoincrement()) @db.UnsignedInt
+ username String @unique() @db.VarChar(255)
+ password String @db.VarChar(60)
+ is_admin Boolean @default(false)
+ created_at DateTime? @default(now()) @db.Timestamp(0)
+ updated_at DateTime? @default(now()) @db.Timestamp(0)
+ account_uuid String @unique() @db.VarChar(36)
+ website website[]
}
model event {
diff --git a/db/postgresql/migrations/04_account_uuid/migration.sql b/db/postgresql/migrations/04_account_uuid/migration.sql
new file mode 100644
index 00000000..f95718d4
--- /dev/null
+++ b/db/postgresql/migrations/04_account_uuid/migration.sql
@@ -0,0 +1,12 @@
+
+-- 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");
diff --git a/db/postgresql/schema.prisma b/db/postgresql/schema.prisma
index a76a3da4..d1d346de 100644
--- a/db/postgresql/schema.prisma
+++ b/db/postgresql/schema.prisma
@@ -8,13 +8,14 @@ datasource db {
}
model account {
- user_id Int @id @default(autoincrement())
- username String @unique @db.VarChar(255)
- password String @db.VarChar(60)
- is_admin Boolean @default(false)
- created_at DateTime? @default(now()) @db.Timestamptz(6)
- updated_at DateTime? @default(now()) @db.Timestamptz(6)
- website website[]
+ user_id Int @id @default(autoincrement())
+ username String @unique @db.VarChar(255)
+ password String @db.VarChar(60)
+ is_admin Boolean @default(false)
+ created_at DateTime? @default(now()) @db.Timestamptz(6)
+ updated_at DateTime? @default(now()) @db.Timestamptz(6)
+ account_uuid String @unique @db.Uuid
+ website website[]
}
model event {
diff --git a/pages/api/account/index.js b/pages/api/account/index.js
deleted file mode 100644
index 236538bc..00000000
--- a/pages/api/account/index.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics';
-import { getAccountByUsername, createAccount } from 'queries';
-import { useAuth } from 'lib/middleware';
-
-export default async (req, res) => {
- if (req.method === 'POST') {
- await useAuth(req, res);
-
- if (!req.auth.is_admin) {
- return unauthorized(res);
- }
-
- const { username, password } = req.body;
-
- const accountByUsername = await getAccountByUsername(username);
-
- if (accountByUsername) {
- return badRequest(res, 'Account already exists');
- }
-
- const created = await createAccount({ username, password: hashPassword(password) });
-
- return ok(res, created);
- }
-
- return methodNotAllowed(res);
-};
diff --git a/pages/api/account/[id].js b/pages/api/accounts/[id]/index.js
similarity index 100%
rename from pages/api/account/[id].js
rename to pages/api/accounts/[id]/index.js
diff --git a/pages/api/account/password.js b/pages/api/accounts/[id]/password.js
similarity index 56%
rename from pages/api/account/password.js
rename to pages/api/accounts/[id]/password.js
index da3a643c..96858205 100644
--- a/pages/api/account/password.js
+++ b/pages/api/accounts/[id]/password.js
@@ -12,24 +12,25 @@ import {
export default async (req, res) => {
await useAuth(req, res);
- const { user_id: auth_user_id, is_admin } = req.auth;
- const { user_id, current_password, new_password } = req.body;
+ const { user_id: currentUserId, is_admin: currentUserIsAdmin } = req.auth;
+ const { current_password, new_password } = req.body;
+ const { id } = req.query;
+ const userId = +id;
- if (!is_admin && user_id !== auth_user_id) {
+ if (!currentUserIsAdmin && userId !== currentUserId) {
return unauthorized(res);
}
if (req.method === 'POST') {
- const account = await getAccountById(user_id);
- const valid = checkPassword(current_password, account.password);
+ const account = await getAccountById(userId);
- if (!valid) {
+ if (!checkPassword(current_password, account.password)) {
return badRequest(res, 'Current password is incorrect');
}
const password = hashPassword(new_password);
- const updated = await updateAccount(user_id, { password });
+ const updated = await updateAccount(userId, { password });
return ok(res, updated);
}
diff --git a/pages/api/accounts/index.js b/pages/api/accounts/index.js
index 42b96a11..acaf1451 100644
--- a/pages/api/accounts/index.js
+++ b/pages/api/accounts/index.js
@@ -1,6 +1,7 @@
-import { getAccounts } from 'queries';
+import { ok, unauthorized, methodNotAllowed, badRequest, hashPassword } from 'next-basics';
import { useAuth } from 'lib/middleware';
-import { ok, unauthorized, methodNotAllowed } from 'next-basics';
+import { uuid } from 'lib/crypto';
+import { createAccount, getAccountByUsername, getAccounts } from 'queries';
export default async (req, res) => {
await useAuth(req, res);
@@ -17,5 +18,29 @@ export default async (req, res) => {
return ok(res, accounts);
}
+ if (req.method === 'POST') {
+ await useAuth(req, res);
+
+ if (!req.auth.is_admin) {
+ return unauthorized(res);
+ }
+
+ const { username, password } = req.body;
+
+ const accountByUsername = await getAccountByUsername(username);
+
+ if (accountByUsername) {
+ return badRequest(res, 'Account already exists');
+ }
+
+ const created = await createAccount({
+ username,
+ password: hashPassword(password),
+ account_uuid: uuid(),
+ });
+
+ return ok(res, created);
+ }
+
return methodNotAllowed(res);
};
diff --git a/pages/api/website/[id]/index.js b/pages/api/website/[id]/index.js
deleted file mode 100644
index aaebb3d4..00000000
--- a/pages/api/website/[id]/index.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { methodNotAllowed, ok, unauthorized } from 'next-basics';
-import { deleteWebsite, getWebsiteById } from 'queries';
-import { allowQuery } from 'lib/auth';
-import { useCors } from 'lib/middleware';
-
-export default async (req, res) => {
- const { id } = req.query;
-
- const websiteId = +id;
-
- if (req.method === 'GET') {
- await useCors(req, res);
-
- if (!(await allowQuery(req))) {
- return unauthorized(res);
- }
-
- const website = await getWebsiteById(websiteId);
-
- return ok(res, website);
- }
-
- if (req.method === 'DELETE') {
- if (!(await allowQuery(req, true))) {
- return unauthorized(res);
- }
-
- await deleteWebsite(websiteId);
-
- return ok(res);
- }
-
- return methodNotAllowed(res);
-};
diff --git a/pages/api/website/index.js b/pages/api/website/index.js
deleted file mode 100644
index ac02de85..00000000
--- a/pages/api/website/index.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { ok, unauthorized, methodNotAllowed, getRandomChars } from 'next-basics';
-import { updateWebsite, createWebsite, getWebsiteById } from 'queries';
-import { useAuth } from 'lib/middleware';
-import { uuid } from 'lib/crypto';
-
-export default async (req, res) => {
- await useAuth(req, res);
-
- const { user_id, is_admin } = req.auth;
- const { website_id, enable_share_url } = req.body;
-
- if (req.method === 'POST') {
- const { name, domain, owner } = req.body;
- const website_owner = parseInt(owner);
-
- if (website_id) {
- const website = await getWebsiteById(website_id);
-
- if (website.user_id !== user_id && !is_admin) {
- return unauthorized(res);
- }
-
- let { share_id } = website;
-
- if (enable_share_url) {
- share_id = share_id ? share_id : getRandomChars(8);
- } else {
- share_id = null;
- }
-
- await updateWebsite(website_id, { name, domain, share_id, user_id: website_owner });
-
- return ok(res);
- } else {
- const website_uuid = uuid();
- const share_id = enable_share_url ? getRandomChars(8) : null;
- const website = await createWebsite(website_owner, { website_uuid, name, domain, share_id });
-
- return ok(res, website);
- }
- }
-
- return methodNotAllowed(res);
-};
diff --git a/pages/api/website/[id]/active.js b/pages/api/websites/[id]/active.js
similarity index 100%
rename from pages/api/website/[id]/active.js
rename to pages/api/websites/[id]/active.js
diff --git a/pages/api/website/[id]/events.js b/pages/api/websites/[id]/events.js
similarity index 100%
rename from pages/api/website/[id]/events.js
rename to pages/api/websites/[id]/events.js
diff --git a/pages/api/websites/[id]/index.js b/pages/api/websites/[id]/index.js
new file mode 100644
index 00000000..bc9cb17f
--- /dev/null
+++ b/pages/api/websites/[id]/index.js
@@ -0,0 +1,59 @@
+import { getRandomChars, methodNotAllowed, ok, unauthorized } from 'next-basics';
+import { deleteWebsite, getWebsiteById, updateWebsite } from 'queries';
+import { allowQuery } from 'lib/auth';
+import { useAuth, useCors } from 'lib/middleware';
+
+export default async (req, res) => {
+ const { id } = req.query;
+
+ const websiteId = +id;
+
+ if (req.method === 'GET') {
+ await useCors(req, res);
+
+ if (!(await allowQuery(req))) {
+ return unauthorized(res);
+ }
+
+ const website = await getWebsiteById(websiteId);
+
+ return ok(res, website);
+ }
+
+ if (req.method === 'POST') {
+ await useAuth(req, res);
+
+ const { is_admin: currentUserIsAdmin, user_id: currentUserId } = req.auth;
+ const { name, domain, owner, enable_share_url } = req.body;
+
+ const website = await getWebsiteById(websiteId);
+
+ if (website.user_id !== currentUserId && !currentUserIsAdmin) {
+ return unauthorized(res);
+ }
+
+ let { share_id } = website;
+
+ if (enable_share_url) {
+ share_id = share_id ? share_id : getRandomChars(8);
+ } else {
+ share_id = null;
+ }
+
+ await updateWebsite(websiteId, { name, domain, share_id, user_id: +owner });
+
+ return ok(res);
+ }
+
+ if (req.method === 'DELETE') {
+ if (!(await allowQuery(req, true))) {
+ return unauthorized(res);
+ }
+
+ await deleteWebsite(websiteId);
+
+ return ok(res);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/pages/api/website/[id]/metrics.js b/pages/api/websites/[id]/metrics.js
similarity index 100%
rename from pages/api/website/[id]/metrics.js
rename to pages/api/websites/[id]/metrics.js
diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/websites/[id]/pageviews.js
similarity index 100%
rename from pages/api/website/[id]/pageviews.js
rename to pages/api/websites/[id]/pageviews.js
diff --git a/pages/api/website/[id]/reset.js b/pages/api/websites/[id]/reset.js
similarity index 100%
rename from pages/api/website/[id]/reset.js
rename to pages/api/websites/[id]/reset.js
diff --git a/pages/api/website/[id]/stats.js b/pages/api/websites/[id]/stats.js
similarity index 100%
rename from pages/api/website/[id]/stats.js
rename to pages/api/websites/[id]/stats.js
diff --git a/pages/api/websites/index.js b/pages/api/websites/index.js
index 8b03a9e9..108a4a8a 100644
--- a/pages/api/websites/index.js
+++ b/pages/api/websites/index.js
@@ -1,6 +1,7 @@
-import { getAllWebsites, getUserWebsites } from 'queries';
+import { createWebsite, getAllWebsites, getUserWebsites } from 'queries';
+import { ok, methodNotAllowed, unauthorized, getRandomChars } from 'next-basics';
import { useAuth } from 'lib/middleware';
-import { ok, methodNotAllowed, unauthorized } from 'next-basics';
+import { uuid } from 'lib/crypto';
export default async (req, res) => {
await useAuth(req, res);
@@ -22,5 +23,24 @@ export default async (req, res) => {
return ok(res, websites);
}
+ if (req.method === 'POST') {
+ await useAuth(req, res);
+
+ const { is_admin: currentUserIsAdmin, user_id: currentUserId } = req.auth;
+ const { name, domain, owner, enable_share_url } = req.body;
+
+ const website_owner = +owner;
+
+ if (website_owner !== currentUserId && !currentUserIsAdmin) {
+ return unauthorized(res);
+ }
+
+ const website_uuid = uuid();
+ const share_id = enable_share_url ? getRandomChars(8) : null;
+ const website = await createWebsite(website_owner, { website_uuid, name, domain, share_id });
+
+ return ok(res, website);
+ }
+
return methodNotAllowed(res);
};
diff --git a/scripts/check-db.js b/scripts/check-db.js
index 4de18a89..d5cc2a64 100644
--- a/scripts/check-db.js
+++ b/scripts/check-db.js
@@ -1,3 +1,4 @@
+/* eslint-disable no-console */
require('dotenv').config();
const { PrismaClient } = require('@prisma/client');
const chalk = require('chalk');
@@ -39,7 +40,7 @@ async function checkConnection() {
async function checkTables() {
try {
- await prisma.account.findFirst();
+ await prisma.$queryRaw`select * from account limit 1`;
success('Database tables found.');
} catch (e) {