This commit is contained in:
Timothy Jaeryang Baek 2026-10-07 15:40:44 +04:00
parent 4b9d31b390
commit 5b0f53e827
3 changed files with 107 additions and 29 deletions

View file

@ -0,0 +1,78 @@
<script context="module" lang="ts">
import type { MfaChallenge } from '$lib/apis/auths/mfa';
export type MfaManagementFlow =
| { challenge: MfaChallenge }
| { code: string; recovery: boolean; token: string };
</script>
<script lang="ts">
import { getContext, onMount } from 'svelte';
import { goto } from '$app/navigation';
import { mfaRequest } from '$lib/apis/auths/mfa';
import { user } from '$lib/stores';
import MfaChallengeForm from './MfaChallenge.svelte';
import MfaRecoveryCodes from './MfaRecoveryCodes.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n: any = getContext('i18n');
export let flow: MfaManagementFlow;
export let onClose: () => void;
let codes: string[] = [];
let error = '';
let reauthenticate = false;
const signIn = async () => {
localStorage.removeItem('token');
user.set(undefined);
await goto('/auth?state=logout&form=signin', { replaceState: true });
onClose();
};
const cancel = async () => {
if (!$user) await signIn();
else onClose();
};
onMount(async () => {
if ('challenge' in flow) return;
try {
const response = await mfaRequest(
'recovery/codes',
{ code: flow.code, recovery: flow.recovery },
flow.token
);
codes = response.recovery_codes;
} catch (e) {
reauthenticate = e instanceof Error && e.message === 'reauthentication_required';
error = reauthenticate
? $i18n.t('Sign in again before changing your authenticator settings.')
: e instanceof Error
? e.message
: String(e);
}
});
</script>
<main id="main-content" class="flex min-h-screen items-center justify-center px-5 py-10">
<div class="w-full max-w-sm">
{#if 'challenge' in flow}
<MfaChallengeForm challenge={flow.challenge} onComplete={signIn} onCancel={cancel} />
{:else if codes.length}
<MfaRecoveryCodes {codes} onContinue={signIn} />
{:else if error}
<p role="alert" class="text-xs text-red-500">{error}</p>
<button
type="button"
class="mt-3 text-xs text-gray-500 hover:text-gray-700 dark:hover:text-gray-200"
on:click={reauthenticate ? signIn : cancel}
>
{reauthenticate || !$user ? $i18n.t('Sign in again') : $i18n.t('Back')}
</button>
{:else}
<div role="status" class="flex items-center justify-center gap-2 text-xs text-gray-500">
<Spinner />
{$i18n.t('Loading...')}
</div>
{/if}
</div>
</main>

View file

@ -1,13 +1,11 @@
<script lang="ts">
import { onMount, getContext } from 'svelte';
import { mfaRequest, type MfaChallenge } from '$lib/apis/auths/mfa';
import MfaChallengeForm from '$lib/components/auth/MfaChallenge.svelte';
import MfaRecoveryCodes from '$lib/components/auth/MfaRecoveryCodes.svelte';
import { mfaRequest } from '$lib/apis/auths/mfa';
import type { MfaManagementFlow } from '$lib/components/auth/MfaManagement.svelte';
const i18n: any = getContext('i18n');
const showMfaManagement = getContext<(flow: MfaManagementFlow) => void>('showMfaManagement');
let status: { enabled: boolean; required: boolean; recovery_codes_remaining: number } | null =
null;
let challenge: MfaChallenge | null = null;
let codes: string[] = [];
let code = '';
let recovery = false;
let busy = false;
@ -28,17 +26,18 @@
window.location.href = '/auth?state=logout&form=signin';
};
const manage = async (replace: boolean) => {
if (busy) return;
if (!replace) {
showMfaManagement({ code, recovery, token: localStorage.token });
code = '';
return;
}
busy = true;
error = '';
try {
const response = await mfaRequest(
replace ? 'replace' : 'recovery/codes',
{ code, recovery },
localStorage.token
);
const response = await mfaRequest('replace', { code, recovery }, localStorage.token);
code = '';
if (replace) challenge = response;
else codes = response.recovery_codes;
showMfaManagement({ challenge: response });
} catch (e) {
reauthenticate = e instanceof Error && e.message === 'reauthentication_required';
error = reauthenticate
@ -53,19 +52,7 @@
</script>
<div class="space-y-2.5 text-xs">
{#if codes.length}
<div class="w-full sm:max-w-md"><MfaRecoveryCodes {codes} onContinue={signIn} /></div>
{:else if challenge}
<div class="w-full sm:max-w-md">
<MfaChallengeForm
{challenge}
onComplete={signIn}
onCancel={() => {
challenge = null;
}}
/>
</div>
{:else if status}
{#if status}
<div class="flex items-center justify-between gap-2.5">
<span class="text-gray-600 dark:text-gray-400"
>{status.enabled

View file

@ -79,6 +79,7 @@
import AppSidebar from '$lib/components/app/AppSidebar.svelte';
import SyncStatsModal from '$lib/components/chat/Settings/SyncStatsModal.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
import MfaManagement from '$lib/components/auth/MfaManagement.svelte';
import { getOutputText } from '$lib/components/chat/Messages/structuredOutput';
import { getUserSettings } from '$lib/apis/users';
import dayjs from 'dayjs';
@ -101,13 +102,22 @@
// handle frontend updates (https://svelte.dev/docs/kit/configuration#version)
beforeNavigate(async ({ willUnload, to }) => {
if (updated.current && !willUnload && to?.url) {
if (updated.current && !mfaManagement && !willUnload && to?.url) {
await unregisterServiceWorkers();
location.href = to.url.href;
}
});
setContext('i18n', i18n);
/** @type {import('$lib/components/auth/MfaManagement.svelte').MfaManagementFlow | null} */
let mfaManagement = null;
setContext(
'showMfaManagement',
/** @param {import('$lib/components/auth/MfaManagement.svelte').MfaManagementFlow} flow */
(flow) => {
mfaManagement = flow;
}
);
const bc = new BroadcastChannel('active-tab-channel');
@ -208,8 +218,9 @@
if (version !== null || deploymentId !== null) {
if (
($WEBUI_VERSION !== null && version !== $WEBUI_VERSION) ||
($WEBUI_DEPLOYMENT_ID !== null && deploymentId !== $WEBUI_DEPLOYMENT_ID)
!mfaManagement &&
(($WEBUI_VERSION !== null && version !== $WEBUI_VERSION) ||
($WEBUI_DEPLOYMENT_ID !== null && deploymentId !== $WEBUI_DEPLOYMENT_ID))
) {
await unregisterServiceWorkers();
location.href = location.href;
@ -1465,7 +1476,9 @@
{/if}
{#if loaded}
{#if $isApp}
{#if mfaManagement}
<MfaManagement flow={mfaManagement} onClose={() => (mfaManagement = null)} />
{:else if $isApp}
<div class="flex flex-row h-screen">
<AppSidebar />