diff --git a/frontend/src/components/overlays/Settings.svelte b/frontend/src/components/overlays/Settings.svelte index 6264f4f..9dc3434 100644 --- a/frontend/src/components/overlays/Settings.svelte +++ b/frontend/src/components/overlays/Settings.svelte @@ -16,21 +16,13 @@ }); }; - 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" }); - } - } - }); + const doSuperuserPrompt = async () => { + const { ok } = await request("POST", apiRoute("users/self/promote"), true); + if (ok) { + overlayStore.open("toast", { message: "You have been promoted to superuser" }); + } else { + overlayStore.open("toast", { message: "Failed to promote to superuser" }); + } }; diff --git a/frontend/src/request.js b/frontend/src/request.js index c054071..6d56715 100644 --- a/frontend/src/request.js +++ b/frontend/src/request.js @@ -1,4 +1,6 @@ import { getItem } from "./storage"; +// TODO: circular dependency +import { overlayStore } from "./stores"; export function compatibleFetch(endpoint, options) { if (window.fetch && typeof window.fetch === "function") { @@ -30,43 +32,71 @@ export function compatibleFetch(endpoint, options) { } } -export default async function(method, endpoint, auth=true, body=null) { - const options = { - method, - }; - - if (body) { - options.body = JSON.stringify(body); - options.headers = { - ...options.headers || {}, - "Content-Type": "application/json" +export default function doRequest(method, endpoint, auth=true, body=null, _keyEntryDepth=false) { + return new Promise(async (resolve, reject) => { + const options = { + method, }; - } - - if (auth) { - const token = getItem("auth:token"); - if (token) { + + if (body) { + options.body = JSON.stringify(body); options.headers = { ...options.headers || {}, - "Authorization": `Bearer ${token}` + "Content-Type": "application/json" }; } - } - - try { - const res = await compatibleFetch(endpoint, options); - return { - success: true, - json: res.status === 204 ? null : await res.json(), - ok: res.ok, - status: res.status + + if (auth) { + const token = getItem("auth:token"); + if (token) { + options.headers = { + ...options.headers || {}, + "Authorization": `Bearer ${token}` + }; + } } - } catch (e) { - return { - success: false, - json: null, - ok: false, - status: null + + try { + const res = await compatibleFetch(endpoint, options); + const json = res.status === 204 ? {} : await res.json(); + + if (res.status === 403 && json.code && json.code === 6006 && !_keyEntryDepth) { + // This endpoint is password-protected + overlayStore.open("prompt", { + heading: "Enter Key For Resource", + valueName: "Key", + async onSubmit(value) { + const response = await doRequest(method, endpoint, auth, { + ...(body || {}), + requestKey: value + }, true); + resolve(response); + }, + onClose() { + resolve({ + success: true, + json, + ok: res.ok, + status: res.status + }); + } + }); + return; + } + + return resolve({ + success: true, + json, + ok: res.ok, + status: res.status + }); + } catch (e) { + return resolve({ + success: false, + json: null, + ok: false, + status: null + }); } - } + }); } diff --git a/src/errors.ts b/src/errors.ts index ea2733b..37f7f4a 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,7 +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" }, + BAD_REQUEST_KEY: { code: 6006, message: "Bad request key" }, GOT_NO_DATABASE_DATA: { code: 7001, message: "Unexpectedly got no data from database" }, FEATURE_DISABLED: { code: 7002, message: "This feature is disabled" }, INTERNAL_ERROR: { code: 7003, message: "Internal server error" } diff --git a/src/routes/api/v1/users.ts b/src/routes/api/v1/users.ts index c8642b3..5f59b88 100644 --- a/src/routes/api/v1/users.ts +++ b/src/routes/api/v1/users.ts @@ -2,14 +2,13 @@ import { errors } from "../../../errors"; import { query } from "../../../database"; import express from "express"; import { body, validationResult } from "express-validator"; -import { compare, hash } from "bcrypt"; +import { compare, hash, hashSync } from "bcrypt"; import { authenticateRoute, signToken } from "../../../auth"; -import { dispatch } from "../../../gateway"; -import { GatewayPayloadType } from "../../../gateway/gatewaypayloadtype"; const router = express.Router(); -const superuserKey = process.env.SUPERUSER_KEY || ""; +const superuserKey = process.env.SUPERUSER_KEY ? hashSync(process.env.SUPERUSER_KEY, 10) : null; +const authRequestKey = process.env.AUTH_REQUEST_KEY ? hashSync(process.env.AUTH_REQUEST_KEY, 10) : null; router.post( "/register", @@ -20,6 +19,16 @@ router.post( return res.status(403).json({ ...errors.FEATURE_DISABLED }); } + if (authRequestKey) { + if (!req.body.requestKey) { + return res.status(403).json({ ...errors.BAD_REQUEST_KEY }); + } + const result = await compare(req.body.requestKey, authRequestKey); + if (!result) { + return res.status(403).json({ ...errors.BAD_REQUEST_KEY }); + } + } + const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.INVALID_DATA, errors: validationErrors.array() }); @@ -52,6 +61,16 @@ router.post( body("username").isLength({ min: 3, max: 32 }).isAlphanumeric("en-US", { ignore: " _-" }), body("password").isLength({ min: 8, max: 1000 }), async (req, res) => { + if (authRequestKey) { + if (!req.body.requestKey) { + return res.status(403).json({ ...errors.BAD_REQUEST_KEY }); + } + const result = await compare(req.body.requestKey, authRequestKey); + if (!result) { + return res.status(403).json({ ...errors.BAD_REQUEST_KEY }); + } + } + const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { return res.status(400).json({ ...errors.BAD_LOGIN_CREDENTIALS }); @@ -85,17 +104,20 @@ router.get( router.post( "/self/promote", authenticateRoute(), - body("key").isLength({ min: 1, max: 3000 }), + body("requestKey").isLength({ min: 1, max: 3000 }), async (req, res) => { + if (!superuserKey) { + return res.status(403).json({ ...errors.FEATURE_DISABLED }); + } const validationErrors = validationResult(req); if (!validationErrors.isEmpty()) { - return res.status(403).json({ ...errors.BAD_SUPERUSER_KEY }); + return res.status(403).json({ ...errors.BAD_REQUEST_KEY }); } - const { key } = req.body; + const matches = await compare(req.body.requestKey, superuserKey); - if (superuserKey && superuserKey.length >= 1 && key === superuserKey && req.user) { + if (matches) { const updateUserResult = await query("UPDATE users SET is_superuser = true WHERE id = $1", [req.user.id]); if (!updateUserResult || updateUserResult.rowCount < 1) { return res.status(500).json({ @@ -105,7 +127,7 @@ router.post( return res.status(200).json({}); } - return res.status(403).json({ ...errors.BAD_SUPERUSER_KEY }); + return res.status(403).json({ ...errors.BAD_REQUEST_KEY }); } );