mirror of
https://github.com/appwrite/console.git
synced 2026-06-06 19:27:48 +00:00
refactor(auth): move mfa challenge to its own component
This will allow us to reuse the component in other places.
This commit is contained in:
@@ -73,3 +73,4 @@ export { default as RadioBoxes } from './radioBoxes.svelte';
|
||||
export { default as ModalWrapper } from './modalWrapper.svelte';
|
||||
export { default as ModalSideCol } from './modalSideCol.svelte';
|
||||
export { default as ImagePreview } from './imagePreview.svelte';
|
||||
export { default as MfaChallengeFormList } from './mfaChallengeFormList.svelte';
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
<script context="module" lang="ts">
|
||||
export async function verify(challenge: Models.MfaChallenge, code: string) {
|
||||
try {
|
||||
if (challenge == null) {
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(
|
||||
AuthenticationFactor.Totp
|
||||
);
|
||||
}
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
} catch (error) {
|
||||
trackError(error, Submit.AccountCreate);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { invalidate } from '$app/navigation';
|
||||
import { Button, FormItem, FormList, InputDigits, InputText } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
|
||||
export let factors: Models.MfaFactors & { recoveryCode: boolean };
|
||||
/** If true, the form will be submitted automatically when the code is entered. */
|
||||
export let autoSubmit: boolean = true;
|
||||
export let showVerifyButton: boolean = true;
|
||||
export let disabled: boolean = false;
|
||||
export let challenge: Models.MfaChallenge;
|
||||
export let code: string;
|
||||
|
||||
let challengeType: AuthenticationFactor;
|
||||
|
||||
const enabledFactors = Object.entries(factors).filter(([_, enabled]) => enabled);
|
||||
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
disabled = true;
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
disabled = false;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const enabledNonRecoveryFactors = enabledFactors.filter(
|
||||
([factor, _]) => factor != 'recoveryCode'
|
||||
);
|
||||
if (enabledNonRecoveryFactors.length == 1) {
|
||||
if (factors.phone) {
|
||||
createChallenge(AuthenticationFactor.Phone);
|
||||
} else if (factors.email) {
|
||||
createChallenge(AuthenticationFactor.Email);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<FormList>
|
||||
{#if challengeType == AuthenticationFactor.Recoverycode}
|
||||
<p>
|
||||
Enter below one of the recovery codes you received when enabling MFA for this account.
|
||||
</p>
|
||||
<InputText id="recovery-code" bind:value={code} required autofocus />
|
||||
{:else}
|
||||
{#if factors.totp && (challengeType == AuthenticationFactor.Totp || challengeType == null)}
|
||||
<p>Enter below a 6-digit one-time code generated by your authentication app.</p>
|
||||
{:else if challengeType == AuthenticationFactor.Email}
|
||||
<p>A 6-digit verification code was sent to your email, enter it below.</p>
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p>A 6-digit verification code was sent to your phone, enter it below.</p>
|
||||
{/if}
|
||||
<InputDigits bind:value={code} required autofocus {autoSubmit} />
|
||||
{/if}
|
||||
{#if showVerifyButton}
|
||||
<FormItem>
|
||||
<Button fullWidth submit {disabled}>Verify</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if enabledFactors.length > 1}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
{#if factors.totp && challengeType != null && challengeType != AuthenticationFactor.Totp}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Totp)}>
|
||||
<span class="icon-device-mobile u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Authenticator app</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.email && challengeType != AuthenticationFactor.Email}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Email)}>
|
||||
<span class="icon-mail u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Email verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.phone && challengeType != AuthenticationFactor.Phone}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Phone)}>
|
||||
<span class="icon-chat-alt u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Phone verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.recoveryCode && challengeType != AuthenticationFactor.Recoverycode}
|
||||
<FormItem>
|
||||
<Button
|
||||
text
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Recoverycode)}>
|
||||
<span class="text">Use recovery code</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
+14
-123
@@ -1,58 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { goto, invalidate } from '$app/navigation';
|
||||
import { goto } from '$app/navigation';
|
||||
import { base } from '$app/paths';
|
||||
import { Button, Form, FormItem, FormList, InputDigits, InputText } from '$lib/elements/forms';
|
||||
import { addNotification } from '$lib/stores/notifications';
|
||||
import { Button, Form } from '$lib/elements/forms';
|
||||
import { sdk } from '$lib/stores/sdk';
|
||||
import { Unauthenticated } from '$lib/layout';
|
||||
import { Dependencies } from '$lib/constants';
|
||||
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
|
||||
import type { Models } from '@appwrite.io/console';
|
||||
import MfaChallengeFormList, { verify } from '$lib/components/mfaChallengeFormList.svelte';
|
||||
import { page } from '$app/stores';
|
||||
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
|
||||
import { addNotification } from '$lib/stores/notifications.js';
|
||||
|
||||
export let data;
|
||||
|
||||
let code: string;
|
||||
let disabled: boolean;
|
||||
let challenge: Models.MfaChallenge;
|
||||
let challengeType: AuthenticationFactor;
|
||||
|
||||
const factors = data.factors as Models.MfaFactors & { recoveryCode: boolean };
|
||||
const enabledFactors = Object.entries(factors).filter(([_, enabled]) => enabled);
|
||||
|
||||
async function createChallenge(factor: AuthenticationFactor) {
|
||||
challengeType = factor;
|
||||
challenge = await sdk.forConsole.account.createMfaChallenge(factor);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const enabledNonRecoveryFactors = enabledFactors.filter(
|
||||
([factor, _]) => factor != 'recoveryCode'
|
||||
);
|
||||
if (enabledNonRecoveryFactors.length == 1) {
|
||||
if (factors.phone) {
|
||||
createChallenge(AuthenticationFactor.Phone);
|
||||
} else if (factors.email) {
|
||||
createChallenge(AuthenticationFactor.Email);
|
||||
}
|
||||
}
|
||||
});
|
||||
let disabled = false;
|
||||
let challenge: Models.MfaChallenge = null;
|
||||
let code = '';
|
||||
|
||||
async function back() {
|
||||
await sdk.forConsole.account.deleteSession('current');
|
||||
await goto(`${base}/`);
|
||||
}
|
||||
|
||||
async function verify() {
|
||||
async function submit() {
|
||||
disabled = true;
|
||||
try {
|
||||
disabled = true;
|
||||
if (challenge == null) {
|
||||
await createChallenge(AuthenticationFactor.Totp);
|
||||
}
|
||||
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
|
||||
await invalidate(Dependencies.ACCOUNT);
|
||||
trackEvent(Submit.AccountCreate);
|
||||
await verify(challenge, code);
|
||||
if ($page.url.searchParams) {
|
||||
const redirect = $page.url.searchParams.get('redirect');
|
||||
$page.url.searchParams.delete('redirect');
|
||||
@@ -65,12 +38,11 @@
|
||||
await goto(`${base}/console`);
|
||||
}
|
||||
} catch (error) {
|
||||
disabled = false;
|
||||
addNotification({
|
||||
type: 'error',
|
||||
message: error.message
|
||||
});
|
||||
trackError(error, Submit.AccountCreate);
|
||||
disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -91,89 +63,8 @@
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="title">Verify your identity</svelte:fragment>
|
||||
<svelte:fragment>
|
||||
<Form onSubmit={verify}>
|
||||
<FormList>
|
||||
{#if factors.totp && (challengeType == AuthenticationFactor.Totp || challengeType == null)}
|
||||
<p class="body-text-1">
|
||||
Enter below a 6-digit one-time code generated by your authentication app.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Email}
|
||||
<p class="body-text-1">
|
||||
A 6-digit verification code was sent to your email, enter it below.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Phone}
|
||||
<p class="body-text-1">
|
||||
A 6-digit verification code was sent to your phone, enter it below.
|
||||
</p>
|
||||
<InputDigits bind:value={code} required autofocus />
|
||||
{:else if challengeType == AuthenticationFactor.Recoverycode}
|
||||
<p class="body-text-1">
|
||||
Enter below one of the recovery codes you received when enabling MFA for
|
||||
this account.
|
||||
</p>
|
||||
<InputText id="recovery-code" bind:value={code} required autofocus />
|
||||
{/if}
|
||||
<FormItem>
|
||||
<Button fullWidth submit {disabled}>Verify</Button>
|
||||
</FormItem>
|
||||
{#if enabledFactors.length > 1}
|
||||
<span class="with-separators eyebrow-heading-3">or</span>
|
||||
<div class="u-flex-vertical u-gap-8">
|
||||
{#if factors.totp && challengeType != null && challengeType != AuthenticationFactor.Totp}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Totp)}>
|
||||
<span
|
||||
class="icon-device-mobile u-font-size-20"
|
||||
aria-hidden="true" />
|
||||
<span class="text">Authenticator app</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.email && challengeType != AuthenticationFactor.Email}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Email)}>
|
||||
<span class="icon-mail u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Email verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.phone && challengeType != AuthenticationFactor.Phone}
|
||||
<FormItem>
|
||||
<Button
|
||||
secondary
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() => createChallenge(AuthenticationFactor.Phone)}>
|
||||
<span class="icon-chat-alt u-font-size-20" aria-hidden="true" />
|
||||
<span class="text">Phone verification</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
{#if factors.recoveryCode && challengeType != AuthenticationFactor.Recoverycode}
|
||||
<FormItem>
|
||||
<Button
|
||||
text
|
||||
fullWidth
|
||||
{disabled}
|
||||
on:click={() =>
|
||||
createChallenge(AuthenticationFactor.Recoverycode)}>
|
||||
<span class="text">Use recovery code</span>
|
||||
</Button>
|
||||
</FormItem>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</FormList>
|
||||
<Form onSubmit={submit} class="body-text-1">
|
||||
<MfaChallengeFormList {factors} bind:challenge bind:code bind:disabled />
|
||||
</Form>
|
||||
</svelte:fragment>
|
||||
</Unauthenticated>
|
||||
|
||||
Reference in New Issue
Block a user