devmode menu and some UI improvements

This commit is contained in:
c0repwn3r 2023-02-24 09:53:32 -05:00
parent 2798d06c81
commit 5174fa0b61
Signed by: core
GPG Key ID: FDBF740DADDCEECF
9 changed files with 180 additions and 21 deletions

View File

@ -1,8 +1,12 @@
<script lang="ts"> <script lang="ts">
import {t} from "$lib/i18n"; import {t} from "$lib/i18n";
import {getCurrentLocale, locales} from "$lib/i18n.js"; import {getCurrentLocale, locales} from "$lib/i18n";
import {locale} from "$lib/stores/LocaleStore"; import {locale} from "$lib/stores/LocaleStore";
import {theme} from "$lib/stores/ThemeStore"; import {theme} from "$lib/stores/ThemeStore";
import {devmode} from "$lib/stores/DevmodeStore";
import {getCookie} from "$lib/cookie";
import {setCookie} from "$lib/cookie";
import {get_user_info, renderDevMenu} from "$lib/auth";
function toggleTheme() { function toggleTheme() {
if ($theme === "dark") { if ($theme === "dark") {
@ -17,17 +21,31 @@
window.location.reload(); window.location.reload();
} }
let dropdownOpen = false; let langDropdownOpen = false;
function toggleDropdown() { function toggleLangDropdown() {
dropdownOpen = !dropdownOpen; langDropdownOpen = !langDropdownOpen;
} }
function closeIfOpen() { function closeLangIfOpen() {
if (dropdownOpen) { if (langDropdownOpen) {
dropdownOpen = !dropdownOpen; langDropdownOpen = !langDropdownOpen;
} }
} }
let devDropdownOpen = false;
function toggleDevDropdown() {
devDropdownOpen = !devDropdownOpen;
}
function closeDevIfOpen() {
if (devDropdownOpen) {
devDropdownOpen = !devDropdownOpen;
}
}
let enableConfirm = false;
</script> </script>
<header> <header>
@ -37,22 +55,25 @@
<div class="inline-block text-sm leading-none lg:m-0"> <div class="inline-block text-sm leading-none lg:m-0">
<div class="relative inline-block"> <div class="relative inline-block">
<div> <div>
<button on:click={toggleDropdown} type="button" <button on:click={toggleLangDropdown} type="button"
class="inline-flex w-full justify-center rounded-md px-4 py-2" id="menu-button" class="inline-flex w-full justify-center rounded-md px-4 py-2" id="lang-menu-button"
aria-expanded="true" aria-haspopup="true" title="{t('header.changeLang')}"> aria-expanded="true" aria-haspopup="true" title="{t('header.changeLang')}">
<i class="fa-solid fa-language"></i> <i class="fa-solid fa-language"></i>
</button> </button>
</div> </div>
<div class:hidden={!dropdownOpen} <div class:hidden={!langDropdownOpen}
class="transition absolute right-0 z-10 w-min mt-2 origin-top-right rounded-md bg-slate-100 dark:bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none" class="transition absolute right-0 z-10 w-min mt-2 origin-top-right rounded-md bg-slate-100 dark:bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu" aria-orientation="vertical" aria-labelledby="menu-button" tabindex="-1" on:mouseleave={closeIfOpen}> role="menu" aria-orientation="vertical" aria-labelledby="lang-menu-button" tabindex="-1"
on:mouseleave={closeLangIfOpen}>
<div class="p-2" role="none"> <div class="p-2" role="none">
<div class="whitespace-nowrap rounded p-2 bg-slate-200 dark:bg-slate-600" role="menuitem" tabindex="-1"> <div class="whitespace-nowrap rounded p-2 bg-slate-200 dark:bg-slate-600" role="menuitem"
tabindex="-1">
<div> <div>
<span class="mr-3 fi fi-{t('common.flag')}"></span> <span class="mr-3 fi fi-{t('common.flag')}"></span>
<span class="mt-0.2 mr-0.5">{t("common.localeName")} <span class="text-slate-400">- {t("common.selected")}</span></span> <span class="mt-0.2 mr-0.5">{t("common.localeName")} <span
class="text-slate-400">- {t("common.selected")}</span></span>
</div> </div>
</div> </div>
@ -84,6 +105,101 @@
<i class="fa-solid fa-moon"></i> <i class="fa-solid fa-moon"></i>
</button> </button>
{/if} {/if}
{#if renderDevMenu()}
<button title="Developer menu" class="inline-block text-sm px-4 leading-none mt-4 lg:mt-0"
id="dev-menu-button" aria-expanded="true" aria-haspopup="true"
on:click={toggleDevDropdown}>
<i class="fa-solid fa-wrench"></i>
</button>
<div class:hidden={!devDropdownOpen}
class="transition absolute right-0 z-10 mr-10 w-max mt-2 origin-top-right rounded-md bg-slate-100 dark:bg-slate-700 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu" aria-orientation="vertical" aria-labelledby="dev-menu-button" tabindex="-1"
on:mouseleave={closeDevIfOpen}>
{#if $devmode === "false"}
{#if !enableConfirm}
<div class="p-2" role="none">
<div class="rounded p-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1" on:click={() => {enableConfirm = true}}>
<div>
<span class="mt-0.2">Enable developer mode</span>
</div>
</div>
</div>
{:else}
<div class="p-2" role="none">
<div class="rounded p-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1" on:click={() => {devmode.set(true)}}>
<div>
<span class="mt-0.2">Are you sure?</span>
</div>
</div>
</div>
{/if}
{:else}
<div class="p-2" role="none">
<div class="rounded p-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {navigator.clipboard.writeText(getCookie("sessionToken"))}}>
<div>
<span class="mt-0.2">Copy session token</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {navigator.clipboard.writeText(getCookie("authToken"))}}>
<div>
<span class="mt-0.2">Copy mfa token</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {navigator.clipboard.writeText(getCookie("sessionToken") + " " + getCookie("authToken"))}}>
<div>
<span class="mt-0.2">Copy API key</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {setCookie("authToken", "", -1); window.location.reload()}}>
<div>
<span class="mt-0.2">Clear MFA state</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={() => {setCookie("authToken", "", -1); setCookie("sessionToken", "", -1); window.location.reload()}}>
<div>
<span class="mt-0.2">Clear login state</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1"
on:click={async () => {await navigator.clipboard.writeText(JSON.stringify(await get_user_info(getCookie("sessionToken") + " " + getCookie("authToken"))))}}>
<div>
<span class="mt-0.2">Copy user profile</span>
</div>
</div>
<div class="rounded p-2 mt-2 hover:bg-slate-300 hover:dark:bg-slate-800 transition"
role="menuitem" tabindex="-1" on:click={() => {devmode.set("false")}}>
<div>
<span class="mt-0.2">Disable developer mode</span>
</div>
</div>
</div>
{/if}
</div>
{/if}
</div> </div>

View File

@ -2,6 +2,7 @@ import {fetch_timeout} from "./util";
import {API_ROOT} from "./config"; import {API_ROOT} from "./config";
import {Logger, logSetup} from "./logger"; import {Logger, logSetup} from "./logger";
import {getCookie, setCookie} from "./cookie"; import {getCookie, setCookie} from "./cookie";
import {browser} from "$app/environment";
logSetup(); logSetup();
const logger = new Logger("auth.ts"); const logger = new Logger("auth.ts");
@ -121,6 +122,7 @@ export async function enforce_auth(): Promise<[boolean, string]> {
return [false, rawerror]; return [false, rawerror];
} else { } else {
// session ok // session ok
logger.info("MFA token is OK");
return [true, `${session_token} ${auth_token}`]; return [true, `${session_token} ${auth_token}`];
} }
} catch (e) { } catch (e) {
@ -129,3 +131,13 @@ export async function enforce_auth(): Promise<[boolean, string]> {
return [false, `${e}`] return [false, `${e}`]
} }
} }
export function renderDevMenu() {
if (!browser) return false;
if (localStorage.getItem("allowdev") === "HACKERMAN") {
console.log("allowing dev menu");
return true;
}
console.log("not allowing dev menu");
return false;
}

View File

@ -51,7 +51,7 @@
"mfa": { "mfa": {
"title": "Two-factor authentication", "title": "Two-factor authentication",
"subtitle": "Enter the code displayed on your authenticator app", "subtitle": "Enter the six-digit code displayed on your authenticator app",
"actionButtonText": "Check code", "actionButtonText": "Check code",
"apierror": { "apierror": {
"invalid TOTP code (maybe it expired?)": "Incorrect 2FA code" "invalid TOTP code (maybe it expired?)": "Incorrect 2FA code"
@ -63,7 +63,7 @@
"subtitle": "2FA is required for all trifid accounts. Protect your account with any TOTP-compatible authenticator app.", "subtitle": "2FA is required for all trifid accounts. Protect your account with any TOTP-compatible authenticator app.",
"qrtitle": "Scan the QR code with your authenticator app.", "qrtitle": "Scan the QR code with your authenticator app.",
"secrettitle": "Or, copy this code into your authenticator app.", "secrettitle": "Or, copy this code into your authenticator app.",
"verifytitle": "Enter the code shown on your authenticator app", "verifytitle": "Enter the six-digit code shown on your authenticator app",
"loadingmfa": "Hang on while we load your account...", "loadingmfa": "Hang on while we load your account...",
"actionButtonText": "Add authenticator", "actionButtonText": "Add authenticator",
"apierror": { "apierror": {

View File

@ -0,0 +1,3 @@
import { persist } from "$lib/PersistentStore";
export const devmode = persist("dev", "false");

View File

@ -44,6 +44,12 @@
error = user; error = user;
return; return;
} }
if (!user.data.actor.hasTOTPAuthenticator) {
logger.error('User doesn\'t have MFA setup yet, redirecting');
window.location = "/auth/mfasetup";
return;
}
}); });
async function tryMFACode() { async function tryMFACode() {
@ -61,6 +67,15 @@
setCookie("authToken", resp.token, 86400 * 365); setCookie("authToken", resp.token, 86400 * 365);
window.location = "/admin"; window.location = "/admin";
} }
function validateKeypress(e: KeyboardEvent) {
if (e.charCode < 47 || e.charCode > 57) {
e.preventDefault();
}
if (e.target.value.length >= 6) {
e.preventDefault();
}
}
</script> </script>
@ -77,7 +92,7 @@
<form class="mt-5" action="#" method="POST" on:submit|preventDefault={tryMFACode}> <form class="mt-5" action="#" method="POST" on:submit|preventDefault={tryMFACode}>
<div class="-space-y-px rounded-md shadow-sm"> <div class="-space-y-px rounded-md shadow-sm">
<label for="mfa_token" class="sr-only">{t('mfa.prompt')}</label> <label for="mfa_token" class="sr-only">{t('mfa.prompt')}</label>
<input bind:value={mfa_token} type="number" maxlength="6" id="mfa_token" <input on:keypress={validateKeypress} bind:value={mfa_token} type="number" maxlength="6" id="mfa_token"
class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none"> class="dark:bg-slate-500 bg-gray-200 w-full rounded px-3 py-2 focus:outline-none focus:ring-purple-500 appearance-none">
{#if hasError} {#if hasError}
<span class="text-red-600 text-sm">{error}</span> <span class="text-red-600 text-sm">{error}</span>

View File

@ -66,9 +66,12 @@
}); });
function validateInput(evt) { function validateInput(e: KeyboardEvent) {
if (evt.which < 48 || evt.which > 57) { if (e.charCode < 47 || e.charCode > 57) {
evt.preventDefault(); e.preventDefault();
}
if (e.target.value.length >= 6) {
e.preventDefault();
} }
} }

View File

@ -5,3 +5,4 @@ web_root = "http://localhost:5173"
magic_links_valid_for = 86400 magic_links_valid_for = 86400
session_tokens_valid_for = 86400 session_tokens_valid_for = 86400
totp_verification_valid_for = 3600 totp_verification_valid_for = 3600
data_key = "1f94d6ba57d79845135ba66446478537f3fccb5ec8e5b7eff7262caa0c358858106a8c5d8d4269ed6e9fc4dd00612bc89b4db06c2288bc2b19ae3e7cebcb461d"

View File

@ -0,0 +1,8 @@
CREATE TABLE organizations (
id SERIAL NOT NULL PRIMARY KEY,
owner SERIAL NOT NULL REFERENCES users(id),
ca_key VARCHAR(3072) NOT NULL,
ca_crt VARCHAR(3072) NOT NULL,
iv VARCHAR(128) NOT NULL
);
CREATE INDEX idx_organizations_owner ON organizations(owner);

View File

@ -9,5 +9,6 @@ pub struct TFConfig {
pub web_root: Url, pub web_root: Url,
pub magic_links_valid_for: i64, pub magic_links_valid_for: i64,
pub session_tokens_valid_for: i64, pub session_tokens_valid_for: i64,
pub totp_verification_valid_for: i64 pub totp_verification_valid_for: i64,
pub data_key: String
} }