add initial concept of "superuser" accounts

This commit is contained in:
hippoz 2022-08-03 02:34:15 +03:00
parent 314a7f2be0
commit 6d76dec265
Signed by: hippoz
GPG key ID: 7C52899193467641
14 changed files with 134 additions and 12 deletions

View file

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

View file

@ -50,7 +50,7 @@
<div class="message">
<span class="author">{ message.author_username }</span>
<span class="message-content" class:pending={ message._isPending }>{ message.content }</span>
{#if userInfoStore.value && message.author_id === userInfoStore.value.id}
{#if userInfoStore.value && (message.author_id === userInfoStore.value.id || userInfoStore.value.is_superuser)}
<button class="icon-button icon-button-auto edit-message" on:click="{ () => overlayStore.open('editMessage', { message }) }">
<MoreVerticalIcon />
</button>

View file

@ -42,7 +42,7 @@
{#if channel._hasUnreads}
<span class="unread-indicator"></span>
{/if}
{#if $userInfoStore && channel.owner_id === $userInfoStore.id}
{#if $userInfoStore && (channel.owner_id === $userInfoStore.id || $userInfoStore.is_superuser)}
<button class="icon-button icon-button-auto" on:click|stopPropagation="{ () => overlayStore.open('editChannel', { channel }) }">
<MoreVerticalIcon />
</button>

View file

@ -7,6 +7,7 @@
import CreateAccount from "./CreateAccount.svelte";
import EditMessage from "./EditMessage.svelte";
import Settings from "./Settings.svelte";
import Prompt from "./Prompt.svelte";
</script>
{#if $overlayStore.createChannel}
@ -30,3 +31,6 @@
{#if $overlayStore.settings}
<Settings />
{/if}
{#if $overlayStore.prompt}
<Prompt { ...$overlayStore.prompt } />
{/if}

View file

@ -0,0 +1,53 @@
<script>
import { maybeFade, maybeFly } from "../../animations";
import { quintInOut } from "svelte/easing";
import { overlayStore } from "../../stores";
export let onSubmit = async () => {};
export let onClose = async () => {};
export let heading = "Prompt";
export let valueName = "Value";
let userInput = "";
let buttonsEnabled = true;
const close = async () => {
await onClose();
overlayStore.close("prompt");
};
const save = async () => {
buttonsEnabled = false;
await onSubmit(userInput);
close();
};
const onKeydown = async (e) => {
if (e.code !== "Enter")
return;
await save();
};
</script>
<style>
.full-width {
width: 100%;
}
</style>
<div class="modal-backdrop" transition:maybeFade="{{ duration: 300, easing: quintInOut }}" on:click="{ close }" on:keydown="{ onKeydown }">
<div class="modal" transition:maybeFly="{{ duration: 300, easing: quintInOut, y: 10 }}" on:click|stopPropagation>
<div class="modal-header">
<span class="h4">{ heading }</span>
</div>
<label class="input-label">
{ valueName }
<input class="input full-width" minlength="1" maxlength="32" bind:value={ userInput } />
</label>
<div class="modal-footer">
<button class="button modal-secondary-action" on:click="{ close }">Cancel</button>
<button class="button button-accent modal-primary-action" on:click="{ save }" disabled="{ !buttonsEnabled }">Submit</button>
</div>
</div>
</div>

View file

@ -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" });
}
}
});
};
</script>
<style>
@ -53,7 +72,7 @@
min-height: 420px;
}
.inner-logout-button {
.account-buttons {
margin-left: auto;
}
</style>
@ -64,7 +83,10 @@
<div class="settings-card full-width">
<AtSignIcon />
<span class="h5 top-bar-heading">{ $userInfoStore ? $userInfoStore.username : "" }</span>
<button class="button button-red inner-logout-button" on:click="{ doLogout }">Log Out</button>
<div class="account-buttons">
<button class="button button-red" on:click="{ doSuperuserPrompt }">Become Superuser</button>
<button class="button button-red" on:click="{ doLogout }">Log Out</button>
</div>
</div>
<div class="separator" />

View file

@ -303,6 +303,7 @@ class OverlayStore extends Store {
createAccount: null,
editMessage: null,
settings: null,
prompt: null,
}, "OverlayStore");
}

View file

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

View file

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

View file

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

View file

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

View file

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

5
src/serverconfig.ts Normal file
View file

@ -0,0 +1,5 @@
export default {
superuserRequirement: {
createChannel: true
},
};

3
src/types/user.d.ts vendored
View file

@ -1,5 +1,6 @@
interface User {
password?: string,
username: string,
id: number
id: number,
is_superuser: boolean
}