} size="large" className={styles.logo} />
diff --git a/lib/crypto.js b/lib/crypto.js
index f77ab3fe..497b5fae 100644
--- a/lib/crypto.js
+++ b/lib/crypto.js
@@ -5,6 +5,7 @@ import { JWT, JWE, JWK } from 'jose';
const SALT_ROUNDS = 10;
const KEY = JWK.asKey(Buffer.from(secret()));
+const CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
export function hash(...args) {
return crypto.createHash('sha512').update(args.join('')).digest('hex');
@@ -24,6 +25,14 @@ export function isValidId(s) {
return validate(s);
}
+export function getRandomChars(n) {
+ let s = '';
+ for (let i = 0; i < n; i++) {
+ s += CHARS[Math.floor(Math.random() * CHARS.length)];
+ }
+ return s;
+}
+
export async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
diff --git a/lib/queries.js b/lib/queries.js
index dc598c11..e698f9b5 100644
--- a/lib/queries.js
+++ b/lib/queries.js
@@ -28,6 +28,16 @@ export async function getWebsiteByUuid(website_uuid) {
);
}
+export async function getWebsiteByShareId(share_id) {
+ return runQuery(
+ prisma.website.findOne({
+ where: {
+ share_id,
+ },
+ }),
+ );
+}
+
export async function getUserWebsites(user_id) {
return runQuery(
prisma.website.findMany({
diff --git a/lib/response.js b/lib/response.js
index 2ec4c57f..31ddcd6f 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -8,22 +8,26 @@ export function redirect(res, url) {
return res.status(303).end();
}
-export function badRequest(res, msg) {
+export function badRequest(res, msg = '400 Bad Request') {
return res.status(400).end(msg);
}
-export function unauthorized(res, msg) {
+export function unauthorized(res, msg = '401 Unauthorized') {
return res.status(401).end(msg);
}
-export function forbidden(res, msg) {
+export function forbidden(res, msg = '403 Forbidden') {
return res.status(403).end(msg);
}
-export function methodNotAllowed(res, msg) {
+export function notFound(res, msg = '404 Not Found') {
+ return res.status(404).end(msg);
+}
+
+export function methodNotAllowed(res, msg = '405 Method Not Allowed') {
res.status(405).end(msg);
}
-export function serverError(res, msg) {
+export function serverError(res, msg = '500 Internal Server Error') {
res.status(500).end(msg);
}
diff --git a/package.json b/package.json
index 31ca1b77..337e7b03 100644
--- a/package.json
+++ b/package.json
@@ -53,10 +53,10 @@
"jose": "^1.28.0",
"maxmind": "^4.1.4",
"moment-timezone": "^0.5.31",
- "next": "9.5.2",
+ "next": "^9.5.2",
"promise-polyfill": "^8.1.3",
- "react": "16.13.1",
- "react-dom": "16.13.1",
+ "react": "^16.13.1",
+ "react-dom": "^16.13.1",
"react-redux": "^7.2.1",
"react-simple-maps": "^2.1.2",
"react-spring": "^8.0.27",
diff --git a/pages/404.js b/pages/404.js
index 3c0c232e..9cc200b5 100644
--- a/pages/404.js
+++ b/pages/404.js
@@ -4,7 +4,9 @@ import Layout from 'components/layout/Layout';
export default function Custom404() {
return (
- oops! not found
+
+
oops! page not found
+
);
}
diff --git a/pages/api/share/[id].js b/pages/api/share/[id].js
new file mode 100644
index 00000000..edce6cfd
--- /dev/null
+++ b/pages/api/share/[id].js
@@ -0,0 +1,18 @@
+import { getWebsiteByShareId } from 'lib/queries';
+import { ok, notFound, methodNotAllowed } from 'lib/response';
+
+export default async (req, res) => {
+ const { id } = req.query;
+
+ if (req.method === 'GET') {
+ const website = await getWebsiteByShareId(id);
+
+ if (website) {
+ return ok(res, website);
+ }
+
+ return notFound(res);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/pages/api/user.js b/pages/api/user.js
deleted file mode 100644
index 2b5017be..00000000
--- a/pages/api/user.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import { parseSecureToken } from 'lib/crypto';
-import { ok, badRequest } from 'lib/response';
-
-export default async (req, res) => {
- const { token } = req.body;
-
- try {
- const payload = await parseSecureToken(token);
- return ok(res, payload);
- } catch {
- return badRequest(res);
- }
-};
diff --git a/pages/api/website.js b/pages/api/website.js
index 9d3fa3ae..1ee40807 100644
--- a/pages/api/website.js
+++ b/pages/api/website.js
@@ -1,22 +1,31 @@
import { updateWebsite, createWebsite, getWebsiteById } from 'lib/queries';
import { useAuth } from 'lib/middleware';
-import { uuid } from 'lib/crypto';
+import { uuid, getRandomChars } from 'lib/crypto';
import { ok, unauthorized, methodNotAllowed } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
const { user_id, is_admin } = req.auth;
- const { website_id } = req.body;
+ const { website_id, make_public } = req.body;
if (req.method === 'POST') {
const { name, domain } = req.body;
if (website_id) {
- const website = getWebsiteById(website_id);
+ const website = await getWebsiteById(website_id);
if (website.user_id === user_id || is_admin) {
- await updateWebsite(website_id, { name, domain });
+ let { share_id } = website;
+ console.log('exising id', share_id, website);
+
+ if (make_public) {
+ share_id = share_id ? share_id : getRandomChars(8);
+ } else {
+ share_id = null;
+ }
+
+ await updateWebsite(website_id, { name, domain, share_id });
return ok(res);
}
@@ -24,7 +33,8 @@ export default async (req, res) => {
return unauthorized(res);
} else {
const website_uuid = uuid();
- const website = await createWebsite(user_id, { website_uuid, name, domain });
+ const share_id = make_public ? getRandomChars(8) : null;
+ const website = await createWebsite(user_id, { website_uuid, name, domain, share_id });
return ok(res, website);
}
diff --git a/pages/api/website/[id]/index.js b/pages/api/website/[id]/index.js
index ac4c348c..40e9995f 100644
--- a/pages/api/website/[id]/index.js
+++ b/pages/api/website/[id]/index.js
@@ -3,9 +3,6 @@ import { useAuth } from 'lib/middleware';
import { methodNotAllowed, ok, unauthorized } from 'lib/response';
export default async (req, res) => {
- await useAuth(req, res);
-
- const { user_id, is_admin } = req.auth;
const { id } = req.query;
const website_id = +id;
@@ -16,6 +13,9 @@ export default async (req, res) => {
}
if (req.method === 'DELETE') {
+ await useAuth(req, res);
+ const { user_id, is_admin } = req.auth;
+
const website = await getWebsiteById(website_id);
if (website.user_id === user_id || is_admin) {
diff --git a/pages/api/website/[id]/metrics.js b/pages/api/website/[id]/metrics.js
index 3ed98764..82ab393e 100644
--- a/pages/api/website/[id]/metrics.js
+++ b/pages/api/website/[id]/metrics.js
@@ -1,10 +1,7 @@
import { getMetrics } from 'lib/queries';
-import { useAuth } from 'lib/middleware';
import { ok } from 'lib/response';
export default async (req, res) => {
- await useAuth(req, res);
-
const { id, start_at, end_at } = req.query;
const metrics = await getMetrics(+id, new Date(+start_at), new Date(+end_at));
diff --git a/pages/api/website/[id]/pageviews.js b/pages/api/website/[id]/pageviews.js
index 0aec91e9..5e07c0ab 100644
--- a/pages/api/website/[id]/pageviews.js
+++ b/pages/api/website/[id]/pageviews.js
@@ -1,13 +1,10 @@
import moment from 'moment-timezone';
import { getPageviews } from 'lib/queries';
-import { useAuth } from 'lib/middleware';
import { ok, badRequest } from 'lib/response';
const unitTypes = ['month', 'hour', 'day'];
export default async (req, res) => {
- await useAuth(req, res);
-
const { id, start_at, end_at, unit, tz } = req.query;
if (!moment.tz.zone(tz) || !unitTypes.includes(unit)) {
diff --git a/pages/api/website/[id]/rankings.js b/pages/api/website/[id]/rankings.js
index 20c4e7b5..a930bca9 100644
--- a/pages/api/website/[id]/rankings.js
+++ b/pages/api/website/[id]/rankings.js
@@ -1,13 +1,10 @@
import { getRankings } from 'lib/queries';
-import { useAuth } from 'lib/middleware';
import { ok, badRequest } from 'lib/response';
const sessionColumns = ['browser', 'os', 'device', 'country'];
const pageviewColumns = ['url', 'referrer'];
export default async (req, res) => {
- await useAuth(req, res);
-
const { id, type, start_at, end_at } = req.query;
if (!sessionColumns.includes(type) && !pageviewColumns.includes(type)) {
diff --git a/pages/share/[...id].js b/pages/share/[...id].js
new file mode 100644
index 00000000..4a099e2e
--- /dev/null
+++ b/pages/share/[...id].js
@@ -0,0 +1,39 @@
+import React, { useState, useEffect } from 'react';
+import { useRouter } from 'next/router';
+import Layout from 'components/layout/Layout';
+import WebsiteDetails from 'components/WebsiteDetails';
+import NotFound from 'pages/404';
+import { get } from 'lib/web';
+
+export default function SharePage() {
+ const [websiteId, setWebsiteId] = useState();
+ const [notFound, setNotFound] = useState(false);
+ const router = useRouter();
+ const { id } = router.query;
+
+ async function loadData() {
+ const website = await get(`/api/share/${id?.[0]}`);
+
+ if (website) {
+ setWebsiteId(website.website_id);
+ } else {
+ setNotFound(true);
+ }
+ }
+
+ useEffect(() => {
+ if (id) {
+ loadData();
+ }
+ }, [id]);
+
+ if (!id || notFound) {
+ return
;
+ }
+
+ return (
+
+
+
+ );
+}
diff --git a/pages/website/[...id].js b/pages/website/[...id].js
index 991d393a..0add10aa 100644
--- a/pages/website/[...id].js
+++ b/pages/website/[...id].js
@@ -13,9 +13,11 @@ export default function DetailsPage() {
return null;
}
+ const [websiteId] = id;
+
return (
-
+
);
}
diff --git a/prisma/schema.mysql.prisma b/prisma/schema.mysql.prisma
index 401f81b5..bf8afeab 100644
--- a/prisma/schema.mysql.prisma
+++ b/prisma/schema.mysql.prisma
@@ -75,6 +75,7 @@ model website {
name String
domain String?
created_at DateTime? @default(now())
+ share_id String? @unique
account account @relation(fields: [user_id], references: [user_id])
event event[]
pageview pageview[]
diff --git a/prisma/schema.postgresql.prisma b/prisma/schema.postgresql.prisma
index 6803135a..e03bc087 100644
--- a/prisma/schema.postgresql.prisma
+++ b/prisma/schema.postgresql.prisma
@@ -75,6 +75,7 @@ model website {
created_at DateTime? @default(now())
user_id Int
domain String?
+ share_id String? @unique
account account @relation(fields: [user_id], references: [user_id])
event event[]
pageview pageview[]
diff --git a/sql/schema.mysql.sql b/sql/schema.mysql.sql
index 69ef4f59..fb0349d3 100644
--- a/sql/schema.mysql.sql
+++ b/sql/schema.mysql.sql
@@ -19,6 +19,7 @@ create table website (
user_id int unsigned not null,
name varchar(100) not null,
domain varchar(500),
+ share_id varchar(8) unique,
created_at timestamp default current_timestamp,
foreign key (user_id) references account(user_id) on delete cascade
) ENGINE=InnoDB;
@@ -99,4 +100,4 @@ begin
end if;
end;
-insert into account (username, password, is_admin) values ('admin', '$2a$10$BXHPV7APlV1I6WrKJt1igeJAyVsvbhMTaTAi3nHkUJFGPsYmfZq3y', true);
\ No newline at end of file
+insert into account (username, password, is_admin) values ('admin', '$2a$10$jsVC1XMAIIQtL0On8twztOmAr20YTVcsd4.yJncKspEwsBkeq6VFW', true);
\ No newline at end of file
diff --git a/sql/schema.postgresql.sql b/sql/schema.postgresql.sql
index aa68acbc..6075738a 100644
--- a/sql/schema.postgresql.sql
+++ b/sql/schema.postgresql.sql
@@ -19,6 +19,7 @@ create table website (
user_id int not null references account(user_id) on delete cascade,
name varchar(100) not null,
domain varchar(500),
+ share_id varchar(8) unique,
created_at timestamp with time zone default current_timestamp
);
@@ -68,4 +69,4 @@ create index event_created_at_idx on event(created_at);
create index event_website_id_idx on event(website_id);
create index event_session_id_idx on event(session_id);
-insert into account (username, password, is_admin) values ('admin', '$2a$10$BXHPV7APlV1I6WrKJt1igeJAyVsvbhMTaTAi3nHkUJFGPsYmfZq3y', true);
\ No newline at end of file
+insert into account (username, password, is_admin) values ('admin', '$2a$10$jsVC1XMAIIQtL0On8twztOmAr20YTVcsd4.yJncKspEwsBkeq6VFW', true);
\ No newline at end of file
diff --git a/styles/index.css b/styles/index.css
index 778616c7..490f9ea4 100644
--- a/styles/index.css
+++ b/styles/index.css
@@ -47,7 +47,8 @@ a:visited {
color: var(--primary400);
}
-input,
+input[type='text'],
+input[type='password'],
textarea {
padding: 4px 8px;
font-size: var(--font-size-normal);
@@ -59,11 +60,19 @@ textarea {
flex: 1;
}
+input[type='checkbox'] + label {
+ margin-left: 10px;
+}
+
label {
flex: 1;
margin-right: 20px;
}
+label:empty {
+ flex: 0;
+}
+
dt {
font-weight: 600;
}
diff --git a/yarn.lock b/yarn.lock
index 0e212b1b..289097d5 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5609,7 +5609,7 @@ next-tick@~1.0.0:
resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.0.0.tgz#ca86d1fe8828169b0120208e3dc8424b9db8342c"
integrity sha1-yobR/ogoFpsBICCOPchCS524NCw=
-next@9.5.2:
+next@^9.5.2:
version "9.5.2"
resolved "https://registry.yarnpkg.com/next/-/next-9.5.2.tgz#ef9b77455b32dca0e917c763529de25c11b5c442"
integrity sha512-wasNxEE4tXXalPoUc7B5Ph3tpByIo7IqodE9iHhp61B/3/vG2zi2BGnCJjZQwFeuotUEQl93krz/0Tp4vd0DsQ==
@@ -6873,7 +6873,7 @@ rc@^1.2.7:
minimist "^1.2.0"
strip-json-comments "~2.0.1"
-react-dom@16.13.1:
+react-dom@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.13.1.tgz#c1bd37331a0486c078ee54c4740720993b2e0e7f"
integrity sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==
@@ -6943,7 +6943,7 @@ react-window@^1.8.5:
"@babel/runtime" "^7.0.0"
memoize-one ">=3.1.1 <6"
-react@16.13.1:
+react@^16.13.1:
version "16.13.1"
resolved "https://registry.yarnpkg.com/react/-/react-16.13.1.tgz#2e818822f1a9743122c063d6410d85c1e3afe48e"
integrity sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==