devmode menu and some UI improvements
This commit is contained in:
parent
2798d06c81
commit
5174fa0b61
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
@ -128,4 +130,14 @@ export async function enforce_auth(): Promise<[boolean, string]> {
|
||||||
setCookie("authToken", "", -1);
|
setCookie("authToken", "", -1);
|
||||||
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;
|
||||||
}
|
}
|
|
@ -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": {
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
import { persist } from "$lib/PersistentStore";
|
||||||
|
|
||||||
|
export const devmode = persist("dev", "false");
|
|
@ -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>
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,4 +4,5 @@ base = "http://localhost:8000"
|
||||||
web_root = "http://localhost:5173"
|
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"
|
||||||
|
|
|
@ -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);
|
|
@ -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
|
||||||
}
|
}
|
Loading…
Reference in New Issue