feat(mfa): show email, phone, and recovery options for MFA verification

Show the appropriate text and buttons depending on what factors are
enabled and what challenge has been created.
This commit is contained in:
Steven Nguyen
2024-04-22 10:38:50 -07:00
parent 7ab1c4d465
commit 2826fc0612
2 changed files with 118 additions and 9 deletions
+110 -9
View File
@@ -1,24 +1,50 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto, invalidate } from '$app/navigation';
import { base } from '$app/paths';
import { Button, Form, FormItem, FormList } from '$lib/elements/forms';
import { Button, Form, FormItem, FormList, InputDigits, InputText } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
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 { page } from '$app/stores';
import { AuthenticationFactor } from '@appwrite.io/console';
import InputDigits from '$lib/elements/forms/inputDigits.svelte';
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
let code: string, disabled: boolean;
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);
}
}
});
async function verify() {
try {
disabled = true;
const challenge = await sdk.forConsole.account.createMfaChallenge(
AuthenticationFactor.Totp
);
if (challenge == null) {
await createChallenge(AuthenticationFactor.Totp);
}
await sdk.forConsole.account.updateMfaChallenge(challenge.$id, code);
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountCreate);
@@ -53,11 +79,86 @@
<svelte:fragment>
<Form onSubmit={verify}>
<FormList>
<p>Enter below a 6-digit one-time code generated by your authentication app.</p>
<InputDigits bind:value={code} required autofocus />
{#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>
</svelte:fragment>
+8
View File
@@ -0,0 +1,8 @@
import type { PageLoad } from './$types';
import { sdk } from '$lib/stores/sdk';
export const load: PageLoad = async () => {
return {
factors: await sdk.forConsole.account.listMfaFactors()
};
};