This commit is contained in:
Timothy Jaeryang Baek 2026-10-05 16:11:15 +04:00
parent 8bd2671007
commit bda14fe416
4 changed files with 74 additions and 36 deletions

View file

@ -3,6 +3,7 @@
import { mfaRequest, type MfaChallenge } from '$lib/apis/auths/mfa';
import MfaRecoveryCodes from './MfaRecoveryCodes.svelte';
import ChevronRight from '$lib/components/icons/ChevronRight.svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n: any = getContext('i18n');
export let challenge: MfaChallenge;
export let onComplete: (result: any) => void | Promise<void>;
@ -48,10 +49,10 @@
recovery
}
);
code = '';
setup = null;
if (response.recovery_codes) result = response;
else await onComplete(response);
code = '';
setup = null;
}
} catch (e) {
error = e instanceof Error ? e.message : String(e);
@ -65,10 +66,10 @@
{#if result}
<MfaRecoveryCodes
codes={result.recovery_codes}
onContinue={() => {
onContinue={async () => {
const { recovery_codes, ...completed } = result;
await onComplete(completed);
result = null;
onComplete(completed);
}}
/>
{:else}
@ -122,7 +123,9 @@
on:click={loadSetup}>{$i18n.t('Retry setup')}</button
>{/if}
{/if}
<label class="block text-[0.8125rem] leading-5 font-normal text-left text-gray-600 dark:text-gray-400">
<label
class="block text-[0.8125rem] leading-5 font-normal text-left text-gray-600 dark:text-gray-400"
>
{$i18n.t(
challenge.next_step === 'recover'
? 'Operator recovery token'
@ -133,6 +136,7 @@
<input
class="my-0.5 w-full text-[0.8125rem] leading-5 outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
bind:value={code}
readonly={busy}
placeholder={$i18n.t(
challenge.next_step === 'recover'
? 'Enter your recovery token'
@ -159,10 +163,14 @@
<div class="flex justify-end text-gray-700 dark:text-gray-300">
<button
type="button"
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-gray-200 transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5 disabled:opacity-50 flex justify-center"
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-gray-200 transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5 disabled:opacity-50 flex items-center justify-center gap-1.5"
disabled={busy || !code.trim() || (challenge.next_step === 'enroll' && !setup)}
on:click={verify}>{$i18n.t(busy ? 'Verifying…' : 'Continue')}</button
aria-busy={busy}
on:click={verify}
>
{$i18n.t(busy ? 'Verifying…' : 'Continue')}
{#if busy}<Spinner />{/if}
</button>
</div>
<div
class="flex flex-wrap items-center justify-between gap-2 text-[0.6875rem] text-gray-500 dark:text-gray-400"

View file

@ -1,9 +1,25 @@
<script lang="ts">
import { getContext } from 'svelte';
import Spinner from '$lib/components/common/Spinner.svelte';
const i18n: any = getContext('i18n');
export let codes: string[];
export let onContinue: () => void;
export let onContinue: () => void | Promise<void>;
let saved = false;
let busy = false;
let error = '';
const continueHandler = async () => {
if (busy || !saved) return;
busy = true;
error = '';
try {
await onContinue();
} catch (e) {
error = e instanceof Error ? e.message : String(e);
} finally {
busy = false;
}
};
const download = () => {
const url = URL.createObjectURL(new Blob([codes.join('\n') + '\n'], { type: 'text/plain' }));
@ -39,14 +55,22 @@
class="size-3 rounded-sm accent-gray-900 dark:accent-gray-100"
type="checkbox"
bind:checked={saved}
disabled={busy}
/>{$i18n.t('I have saved my recovery codes')}</label
>
{#if error}<p role="alert" class="text-xs leading-4 text-red-600 dark:text-red-400">
{error}
</p>{/if}
<div class="flex justify-end text-gray-700 dark:text-gray-300">
<button
type="button"
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-gray-200 transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5 disabled:opacity-50 flex justify-center"
disabled={!saved}
on:click={onContinue}>{$i18n.t('Continue')}</button
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-gray-200 transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5 disabled:opacity-50 flex items-center justify-center gap-1.5"
disabled={busy || !saved}
aria-busy={busy}
on:click={continueHandler}
>
{$i18n.t('Continue')}
{#if busy}<Spinner />{/if}
</button>
</div>
</div>

View file

@ -54,9 +54,9 @@
<div class="space-y-2.5 text-xs">
{#if codes.length}
<div class="max-w-sm"><MfaRecoveryCodes {codes} onContinue={signIn} /></div>
<div class="w-full sm:max-w-md"><MfaRecoveryCodes {codes} onContinue={signIn} /></div>
{:else if challenge}
<div class="max-w-sm">
<div class="w-full sm:max-w-md">
<MfaChallengeForm
{challenge}
onComplete={signIn}
@ -86,7 +86,7 @@
: $i18n.t('Managed by your administrator.')}
</p>
{#if show && status.required && status.enabled}
<div class="max-w-sm space-y-2.5 py-1">
<div class="w-full sm:max-w-md space-y-2.5 py-1">
<p class="text-[0.6875rem] leading-4 text-gray-400 dark:text-gray-500">
{$i18n.t('Verify with a fresh code. Changes sign out all devices.')}
</p>

View file

@ -78,7 +78,7 @@
redirectPath = $page.url.searchParams.get('redirect') || '/';
}
goto(redirectPath);
await goto(redirectPath);
localStorage.removeItem('redirectPath');
}
};
@ -261,9 +261,7 @@
>
<div class="w-full px-6 min-h-screen flex flex-col text-center">
{#if mfaChallenge}
<div
class="my-auto mx-auto w-full max-w-sm shrink-0 p-5"
>
<div class="my-auto mx-auto w-full sm:max-w-md shrink-0">
<MfaChallengeForm
challenge={mfaChallenge}
onComplete={setSessionUser}
@ -342,14 +340,16 @@
<div class="flex flex-col mt-4">
{#if mode === 'signup'}
<div class="mb-2">
<label for="name" class="text-sm font-normal text-left mb-1 block"
<label
for="name"
class="text-[0.8125rem] leading-5 font-normal text-left mb-1 block"
>{$i18n.t('Name')}</label
>
<input
bind:value={name}
type="text"
id="name"
class="my-0.5 w-full text-sm outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
class="my-0.5 w-full text-[0.8125rem] leading-5 outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
autocomplete="name"
placeholder={$i18n.t('Enter Your Full Name')}
required
@ -359,13 +359,15 @@
{#if mode === 'ldap'}
<div class="mb-2">
<label for="username" class="text-sm font-normal text-left mb-1 block"
<label
for="username"
class="text-[0.8125rem] leading-5 font-normal text-left mb-1 block"
>{$i18n.t('Username')}</label
>
<input
bind:value={ldapUsername}
type="text"
class="my-0.5 w-full text-sm outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
class="my-0.5 w-full text-[0.8125rem] leading-5 outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
autocomplete="username"
name="username"
id="username"
@ -375,14 +377,16 @@
</div>
{:else}
<div class="mb-2">
<label for="email" class="text-sm font-normal text-left mb-1 block"
<label
for="email"
class="text-[0.8125rem] leading-5 font-normal text-left mb-1 block"
>{$i18n.t('Email')}</label
>
<input
bind:value={email}
type="email"
id="email"
class="my-0.5 w-full text-sm outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
class="my-0.5 w-full text-[0.8125rem] leading-5 outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
autocomplete="email"
name="email"
placeholder={$i18n.t('Enter Your Email')}
@ -392,14 +396,16 @@
{/if}
<div>
<label for="password" class="text-sm font-normal text-left mb-1 block"
<label
for="password"
class="text-[0.8125rem] leading-5 font-normal text-left mb-1 block"
>{$i18n.t('Password')}</label
>
<SensitiveInput
bind:value={password}
type="password"
id="password"
class="my-0.5 w-full text-sm outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
class="my-0.5 w-full text-[0.8125rem] leading-5 outline-hidden bg-transparent placeholder:text-gray-300 dark:placeholder:text-gray-600"
placeholder={$i18n.t('Enter Your Password')}
autocomplete={mode === 'signup' ? 'new-password' : 'current-password'}
name="password"
@ -413,14 +419,14 @@
<div class="mt-2">
<label
for="confirm-password"
class="text-sm font-normal text-left mb-1 block"
class="text-[0.8125rem] leading-5 font-normal text-left mb-1 block"
>{$i18n.t('Confirm Password')}</label
>
<SensitiveInput
bind:value={confirmPassword}
type="password"
id="confirm-password"
class="my-0.5 w-full text-sm outline-hidden bg-transparent"
class="my-0.5 w-full text-[0.8125rem] leading-5 outline-hidden bg-transparent"
placeholder={$i18n.t('Confirm Your Password')}
autocomplete="new-password"
name="confirm-password"
@ -434,7 +440,7 @@
{#if $config?.features.enable_login_form || $config?.features.enable_ldap || form}
{#if mode === 'ldap'}
<button
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-sm py-2.5 disabled:opacity-50 flex justify-center"
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5 disabled:opacity-50 flex justify-center"
type="submit"
disabled={submitting}
>
@ -448,7 +454,7 @@
</button>
{:else}
<button
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-sm py-2.5 disabled:opacity-50 flex justify-center"
class="bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5 disabled:opacity-50 flex justify-center"
type="submit"
disabled={submitting}
>
@ -468,7 +474,7 @@
</button>
{#if $config?.features.enable_signup && !($config?.onboarding ?? false)}
<div class=" mt-4 text-sm text-center">
<div class=" mt-4 text-[0.8125rem] leading-5 text-center">
{mode === 'signin'
? $i18n.t("Don't have an account?")
: $i18n.t('Already have an account?')}
@ -508,7 +514,7 @@
<div class="flex flex-col space-y-2">
{#if $config?.oauth?.providers?.google}
<button
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-sm py-2.5"
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/google/login`;
}}
@ -538,7 +544,7 @@
{/if}
{#if $config?.oauth?.providers?.microsoft}
<button
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-sm py-2.5"
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/microsoft/login`;
}}
@ -569,7 +575,7 @@
{/if}
{#if $config?.oauth?.providers?.github}
<button
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-sm py-2.5"
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/github/login`;
}}
@ -590,7 +596,7 @@
{/if}
{#if $config?.oauth?.providers?.oidc}
<button
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-sm py-2.5"
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/oidc/login`;
}}
@ -620,7 +626,7 @@
{/if}
{#if $config?.oauth?.providers?.feishu}
<button
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-sm py-2.5"
class="flex justify-center items-center bg-gray-700/5 hover:bg-gray-700/10 dark:bg-gray-100/5 dark:hover:bg-gray-100/10 dark:text-gray-300 dark:hover:text-white transition w-full rounded-full font-normal text-[0.8125rem] leading-5 py-2.5"
on:click={() => {
window.location.href = `${WEBUI_BASE_URL}/oauth/feishu/login`;
}}