- {menu.map(item => (
-
setOption(item)}
- >
- {item}
-
- ))}
+ {menu.map(item =>
+ item ? (
+
setOption(item)}
+ >
+ {item}
+
+ ) : null,
+ )}
{typeof children === 'function' ? children(option) : children}
diff --git a/components/layout/MenuLayout.module.css b/components/layout/MenuLayout.module.css
index cddc6c2f..a465913e 100644
--- a/components/layout/MenuLayout.module.css
+++ b/components/layout/MenuLayout.module.css
@@ -18,7 +18,7 @@
}
.option {
- padding: 8px 20px;
+ padding: 8px 16px;
cursor: pointer;
min-width: 140px;
margin-right: 30px;
diff --git a/lib/crypto.js b/lib/crypto.js
index ec9be392..f77ab3fe 100644
--- a/lib/crypto.js
+++ b/lib/crypto.js
@@ -24,11 +24,11 @@ export function isValidId(s) {
return validate(s);
}
-export function hashPassword(password) {
+export async function hashPassword(password) {
return bcrypt.hash(password, SALT_ROUNDS);
}
-export function checkPassword(password, hash) {
+export async function checkPassword(password, hash) {
return bcrypt.compare(password, hash);
}
diff --git a/lib/response.js b/lib/response.js
index 54df0f6c..ee6e3062 100644
--- a/lib/response.js
+++ b/lib/response.js
@@ -8,18 +8,18 @@ export function redirect(res, url) {
return res.status(303).end();
}
-export function badRequest(res) {
- return res.status(400).end();
+export function badRequest(res, msg) {
+ return res.status(400).end(msg);
}
-export function unauthorized(res) {
- return res.status(401).end();
+export function unauthorized(res, msg) {
+ return res.status(401).end(msg);
}
-export function forbidden(res) {
- return res.status(403).end();
+export function forbidden(res, msg) {
+ return res.status(403).end(msg);
}
-export function methodNotAllowed(res) {
- res.status(405).end();
+export function methodNotAllowed(res, msg) {
+ res.status(405).end(msg);
}
diff --git a/lib/web.js b/lib/web.js
index 3be2ef57..b75a6ba7 100644
--- a/lib/web.js
+++ b/lib/web.js
@@ -11,7 +11,12 @@ export const apiRequest = (method, url, body) =>
if (res.ok) {
return res.json();
}
- return res.text();
+
+ if (['post', 'put', 'delete'].includes(method)) {
+ return res.text();
+ }
+
+ return null;
});
function parseQuery(url, params = {}) {
diff --git a/pages/api/account.js b/pages/api/account.js
index f04fae10..ef63607a 100644
--- a/pages/api/account.js
+++ b/pages/api/account.js
@@ -1,15 +1,15 @@
import { getAccounts, getAccount, updateAccount, createAccount } from 'lib/db';
import { useAuth } from 'lib/middleware';
import { hashPassword, uuid } from 'lib/crypto';
-import { ok, unauthorized, methodNotAllowed } from 'lib/response';
+import { ok, unauthorized, methodNotAllowed, badRequest } from 'lib/response';
export default async (req, res) => {
await useAuth(req, res);
- const { user_id: current_user_id, is_admin } = req.auth;
+ const { user_id: current_user_id, is_admin: current_user_is_admin } = req.auth;
if (req.method === 'GET') {
- if (is_admin) {
+ if (current_user_is_admin) {
const accounts = await getAccounts();
return ok(res, accounts);
@@ -19,28 +19,47 @@ export default async (req, res) => {
}
if (req.method === 'POST') {
- const { user_id, username, password } = req.body;
+ const { user_id, username, password, is_admin } = req.body;
if (user_id) {
- const account = getAccount({ user_id });
+ const account = await getAccount({ user_id });
- if (account.user_id === current_user_id || is_admin) {
+ if (account.user_id === current_user_id || current_user_is_admin) {
const data = { password: password ? await hashPassword(password) : undefined };
- if (is_admin) {
- data.username = username;
+ // Only admin can change these fields
+ if (current_user_is_admin) {
+ // Cannot change username of admin
+ if (username !== 'admin') {
+ data.username = username;
+ }
+ data.is_admin = is_admin;
}
- const updated = await updateAccount(user_id, { username, password });
+ if (data.username && account.username !== data.username) {
+ const accountByUsername = await getAccount({ username });
+
+ if (accountByUsername) {
+ return badRequest(res, 'Account already exists');
+ }
+ }
+
+ const updated = await updateAccount(user_id, data);
return ok(res, updated);
}
return unauthorized(res);
} else {
- const account = await createAccount({ username, password: await hashPassword(password) });
+ const accountByUsername = await getAccount({ username });
- return ok(res, account);
+ if (accountByUsername) {
+ return badRequest(res, 'Account already exists');
+ }
+
+ const created = await createAccount({ username, password: await hashPassword(password) });
+
+ return ok(res, created);
}
}
diff --git a/pages/api/account/[id].js b/pages/api/account/[id].js
new file mode 100644
index 00000000..f9cfe0a2
--- /dev/null
+++ b/pages/api/account/[id].js
@@ -0,0 +1,33 @@
+import { getAccount, deleteAccount } from 'lib/db';
+import { useAuth } from 'lib/middleware';
+import { methodNotAllowed, ok, unauthorized } from 'lib/response';
+
+export default async (req, res) => {
+ await useAuth(req, res);
+
+ const { is_admin } = req.auth;
+ const { id } = req.query;
+ const user_id = +id;
+
+ if (req.method === 'GET') {
+ if (is_admin) {
+ const account = await getAccount({ user_id });
+
+ return ok(res, account);
+ }
+
+ return unauthorized(res);
+ }
+
+ if (req.method === 'DELETE') {
+ if (is_admin) {
+ await deleteAccount(user_id);
+
+ return ok(res);
+ }
+
+ return unauthorized(res);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/pages/api/account/password.js b/pages/api/account/password.js
new file mode 100644
index 00000000..8a7df5b9
--- /dev/null
+++ b/pages/api/account/password.js
@@ -0,0 +1,28 @@
+import { getAccount, updateAccount } from 'lib/db';
+import { useAuth } from 'lib/middleware';
+import { badRequest, methodNotAllowed, ok } from 'lib/response';
+import { checkPassword, hashPassword } from 'lib/crypto';
+
+export default async (req, res) => {
+ await useAuth(req, res);
+
+ const { user_id } = req.auth;
+ const { current_password, new_password } = req.body;
+
+ if (req.method === 'POST') {
+ const account = await getAccount({ user_id });
+ const valid = await checkPassword(current_password, account.password);
+
+ if (!valid) {
+ return badRequest(res, 'Current password is incorrect');
+ }
+
+ const password = await hashPassword(new_password);
+
+ const updated = await updateAccount(user_id, { password });
+
+ return ok(res, updated);
+ }
+
+ return methodNotAllowed(res);
+};
diff --git a/pages/api/auth/login.js b/pages/api/auth/login.js
index 89833a9b..2cf688f3 100644
--- a/pages/api/auth/login.js
+++ b/pages/api/auth/login.js
@@ -7,7 +7,7 @@ import { ok, unauthorized } from 'lib/response';
export default async (req, res) => {
const { username, password } = req.body;
- const account = await getAccount(username);
+ const account = await getAccount({ username });
if (account && (await checkPassword(password, account.password))) {
const { user_id, username, is_admin } = account;
diff --git a/public/umami.js b/public/umami.js
index d5580f55..0db8c3ea 100644
--- a/public/umami.js
+++ b/public/umami.js
@@ -1 +1 @@
-!function(){"use strict";function e(e){var t=this.constructor;return this.then((function(n){return t.resolve(e()).then((function(){return n}))}),(function(n){return t.resolve(e()).then((function(){return t.reject(n)}))}))}var t=setTimeout;function n(e){return Boolean(e&&void 0!==e.length)}function r(){}function o(e){if(!(this instanceof o))throw new TypeError("Promises must be constructed via new");if("function"!=typeof e)throw new TypeError("not a function");this._state=0,this._handled=!1,this._value=void 0,this._deferreds=[],f(e,this)}function i(e,t){for(;3===e._state;)e=e._value;0!==e._state?(e._handled=!0,o._immediateFn((function(){var n=1===e._state?t.onFulfilled:t.onRejected;if(null!==n){var r;try{r=n(e._value)}catch(e){return void s(t.promise,e)}u(t.promise,r)}else(1===e._state?u:s)(t.promise,e._value)}))):e._deferreds.push(t)}function u(e,t){try{if(t===e)throw new TypeError("A promise cannot be resolved with itself.");if(t&&("object"==typeof t||"function"==typeof t)){var n=t.then;if(t instanceof o)return e._state=3,e._value=t,void a(e);if("function"==typeof n)return void f((r=n,i=t,function(){r.apply(i,arguments)}),e)}e._state=1,e._value=t,a(e)}catch(t){s(e,t)}var r,i}function s(e,t){e._state=2,e._value=t,a(e)}function a(e){2===e._state&&0===e._deferreds.length&&o._immediateFn((function(){e._handled||o._unhandledRejectionFn(e._value)}));for(var t=0,n=e._deferreds.length;t