diff --git a/frontend/rollup.config.js b/frontend/rollup.config.js index e8965ec..59e7aa0 100644 --- a/frontend/rollup.config.js +++ b/frontend/rollup.config.js @@ -17,7 +17,7 @@ function serve() { return { writeBundle() { if (server) return; - server = require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], { + server = require('child_process').spawn('yarn', ['run', 'start', '--', '--dev'], { stdio: ['ignore', 'inherit', 'inherit'], shell: true }); diff --git a/frontend/src/components/Message.svelte b/frontend/src/components/Message.svelte index 5aaf066..fe8c492 100644 --- a/frontend/src/components/Message.svelte +++ b/frontend/src/components/Message.svelte @@ -50,7 +50,7 @@
{ message.author_username } { message.content } - {#if userInfoStore.value && message.author_id === userInfoStore.value.id} + {#if userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)} diff --git a/frontend/src/components/Sidebar.svelte b/frontend/src/components/Sidebar.svelte index fda2169..1bda3ac 100644 --- a/frontend/src/components/Sidebar.svelte +++ b/frontend/src/components/Sidebar.svelte @@ -42,7 +42,7 @@ {#if channel._hasUnreads} {/if} - {#if $userInfoStore && channel.owner_id === $userInfoStore.id} + {#if $userInfoStore && (channel.owner_id === $userInfoStore.id || $userInfoStore.is_superuser)} diff --git a/frontend/src/components/overlays/OverlayProvider.svelte b/frontend/src/components/overlays/OverlayProvider.svelte index 47a2dc2..9da9b21 100644 --- a/frontend/src/components/overlays/OverlayProvider.svelte +++ b/frontend/src/components/overlays/OverlayProvider.svelte @@ -7,6 +7,7 @@ import CreateAccount from "./CreateAccount.svelte"; import EditMessage from "./EditMessage.svelte"; import Settings from "./Settings.svelte"; + import Prompt from "./Prompt.svelte"; {#if $overlayStore.createChannel} @@ -30,3 +31,6 @@ {#if $overlayStore.settings} {/if} +{#if $overlayStore.prompt} + +{/if} diff --git a/frontend/src/components/overlays/Prompt.svelte b/frontend/src/components/overlays/Prompt.svelte new file mode 100644 index 0000000..6c939fc --- /dev/null +++ b/frontend/src/components/overlays/Prompt.svelte @@ -0,0 +1,53 @@ + + + + + diff --git a/frontend/src/components/overlays/Settings.svelte b/frontend/src/components/overlays/Settings.svelte index beb4aa1..473e12f 100644 --- a/frontend/src/components/overlays/Settings.svelte +++ b/frontend/src/components/overlays/Settings.svelte @@ -4,6 +4,8 @@ import { overlayStore, userInfoStore, smallViewport, theme, doAnimations } from "../../stores"; import { logOut } from "../../auth"; import { maybeFade, maybeFly } from "../../animations"; + import request from "../../request"; + import { apiRoute } from "../../storage"; const close = () => overlayStore.close("settings"); @@ -14,6 +16,23 @@ message: "Logged out" }); }; + + const doSuperuserPrompt = () => { + overlayStore.open("prompt", { + heading: "Become Superuser", + valueName: "Superuser Key", + async onSubmit(value) { + const { ok } = await request("POST", apiRoute("users/self/promote"), true, { + key: value + }); + if (ok) { + overlayStore.open("toast", { message: "You have been promoted to superuser" }); + } else { + overlayStore.open("toast", { message: "Failed to promote to superuser" }); + } + } + }); + }; @@ -64,7 +83,10 @@
{ $userInfoStore ? $userInfoStore.username : "" } - +
diff --git a/frontend/src/stores.js b/frontend/src/stores.js index 826e779..09f4f31 100644 --- a/frontend/src/stores.js +++ b/frontend/src/stores.js @@ -303,6 +303,7 @@ class OverlayStore extends Store { createAccount: null, editMessage: null, settings: null, + prompt: null, }, "OverlayStore"); } diff --git a/src/database/init.ts b/src/database/init.ts index 96d1a5b..cbda67c 100644 --- a/src/database/init.ts +++ b/src/database/init.ts @@ -5,7 +5,8 @@ export default async function databaseInit() { CREATE TABLE IF NOT EXISTS users( id SERIAL PRIMARY KEY, username VARCHAR(32) UNIQUE NOT NULL, - password TEXT + password TEXT, + is_superuser BOOLEAN ); CREATE TABLE IF NOT EXISTS channels( diff --git a/src/errors.ts b/src/errors.ts index 65a4a15..f660013 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,6 +4,7 @@ export const errors = { BAD_AUTH: { code: 6003, message: "Bad authentication" }, NOT_FOUND: { code: 6004, message: "Not found" }, FORBIDDEN_DUE_TO_MISSING_PERMISSIONS: { code: 6005, message: "Forbidden due to missing permission(s)" }, + BAD_SUPERUSER_KEY: { code: 6006, message: "Bad superuser key" }, GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" }, FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" } }; diff --git a/src/routes/api/v1/channels.ts b/src/routes/api/v1/channels.ts index ff882b1..17d2cfa 100644 --- a/src/routes/api/v1/channels.ts +++ b/src/routes/api/v1/channels.ts @@ -6,6 +6,7 @@ import { getMessageById, getMessagesByChannelFirstPage, getMessagesByChannelPage import { errors } from "../../../errors"; import { dispatch, dispatchChannelSubscribe } from "../../../gateway"; import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype"; +import serverConfig from "../../../serverconfig"; const router = express.Router(); @@ -19,6 +20,10 @@ router.post( return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); } + if (serverConfig.superuserRequirement.createChannel && !req.user.is_superuser) { + return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); + } + const { name } = req.body; const result = await query("INSERT INTO channels(name, owner_id) VALUES ($1, $2) RETURNING id, name, owner_id", [name, req.user.id]); if (result.rowCount < 1) { @@ -60,7 +65,7 @@ router.put( ...errors.NOT_FOUND }); } - if (permissionCheckResult.rows[0].owner_id !== req.user.id) { + if (permissionCheckResult.rows[0].owner_id !== req.user.id && !req.user.is_superuser) { return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); @@ -106,7 +111,7 @@ router.delete( ...errors.NOT_FOUND }); } - if (permissionCheckResult.rows[0].owner_id !== req.user.id) { + if (permissionCheckResult.rows[0].owner_id !== req.user.id && !req.user.is_superuser) { return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); diff --git a/src/routes/api/v1/messages.ts b/src/routes/api/v1/messages.ts index 146b8e0..d623014 100644 --- a/src/routes/api/v1/messages.ts +++ b/src/routes/api/v1/messages.ts @@ -27,7 +27,7 @@ router.delete( ...errors.NOT_FOUND }); } - if (permissionCheckResult.rows[0].author_id !== req.user.id) { + if (permissionCheckResult.rows[0].author_id !== req.user.id && !req.user.is_superuser) { return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); @@ -72,7 +72,7 @@ router.put( ...errors.NOT_FOUND }); } - if (permissionCheckResult.rows[0].author_id !== req.user.id) { + if (permissionCheckResult.rows[0].author_id !== req.user.id && !req.user.is_superuser) { return res.status(403).json({ ...errors.FORBIDDEN_DUE_TO_MISSING_PERMISSIONS }); diff --git a/src/routes/api/v1/users.ts b/src/routes/api/v1/users.ts index aec59b0..5f12435 100644 --- a/src/routes/api/v1/users.ts +++ b/src/routes/api/v1/users.ts @@ -7,6 +7,8 @@ import { authenticateRoute, signToken } from "../../../auth"; const router = express.Router(); +const superuserKey = process.env.SUPERUSER_KEY || ""; + router.post( "/register", body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), @@ -32,7 +34,7 @@ router.post( } const hashedPassword = await hash(password, 10); - const insertedUser = await query("INSERT INTO users(username, password) VALUES ($1, $2) RETURNING id, username", [username, hashedPassword]); + const insertedUser = await query("INSERT INTO users(username, password, is_superuser) VALUES ($1, $2, $3) RETURNING id, username, is_superuser", [username, hashedPassword, false]); if (insertedUser.rowCount < 1) { return res.status(500).json({ ...errors.GOT_NO_DATABASE_DATA @@ -78,4 +80,31 @@ router.get( } ); +router.post( + "/self/promote", + authenticateRoute(), + body("key").isLength({ min: 1, max: 3000 }), + async (req, res) => { + + const validationErrors = validationResult(req); + if (!validationErrors.isEmpty()) { + return res.status(403).json({ ...errors.BAD_SUPERUSER_KEY }); + } + + const { key } = req.body; + + if (superuserKey && superuserKey.length >= 1 && key === superuserKey && req.user) { + const updateUserResult = await query("UPDATE users SET is_superuser = true WHERE id = $1", [req.user.id]); + if (updateUserResult.rowCount < 1) { + return res.status(500).json({ + ...errors.GOT_NO_DATABASE_DATA + }); + } + return res.status(200).json({}); + } + + return res.status(403).json({ ...errors.BAD_SUPERUSER_KEY }); + } +); + export default router; diff --git a/src/serverconfig.ts b/src/serverconfig.ts new file mode 100644 index 0000000..6b7bf03 --- /dev/null +++ b/src/serverconfig.ts @@ -0,0 +1,5 @@ +export default { + superuserRequirement: { + createChannel: true + }, +}; diff --git a/src/types/user.d.ts b/src/types/user.d.ts index 85cab31..160f52a 100644 --- a/src/types/user.d.ts +++ b/src/types/user.d.ts @@ -1,5 +1,6 @@ interface User { password?: string, username: string, - id: number + id: number, + is_superuser: boolean }