Files
console/src/lib/components/mfaChallengeFormList.svelte
T
2025-09-13 11:52:53 +05:30

200 lines
7.3 KiB
Svelte

<script context="module" lang="ts">
export async function verify(
challenge: Models.MfaChallenge,
code: string,
challengeType: AuthenticationFactor = AuthenticationFactor.Totp,
factors?: Models.MfaFactors & { recoveryCode: boolean }
) {
try {
if (!challenge) {
// Validate that the challengeType is actually enabled
if (factors) {
const factorMap = {
[AuthenticationFactor.Totp]: factors.totp,
[AuthenticationFactor.Email]: factors.email,
[AuthenticationFactor.Phone]: factors.phone,
[AuthenticationFactor.Recoverycode]: factors.recoveryCode
};
if (!factorMap[challengeType]) {
throw new Error(`Authentication factor ${challengeType} is not enabled`);
}
}
challenge = await sdk.forConsole.account.createMFAChallenge({
factor: challengeType
});
}
await sdk.forConsole.account.updateMFAChallenge({
challengeId: challenge.$id,
otp: code
});
await invalidate(Dependencies.ACCOUNT);
trackEvent(Submit.AccountLogin, { mfa_used: true });
} catch (error) {
trackError(error, Submit.AccountLogin);
throw error;
}
}
</script>
<script lang="ts">
import { onMount } from 'svelte';
import { sdk } from '$lib/stores/sdk';
import { invalidate } from '$app/navigation';
import { Dependencies } from '$lib/constants';
import { Submit, trackEvent, trackError } from '$lib/actions/analytics';
import { AuthenticationFactor, type Models } from '@appwrite.io/console';
import { Button, InputText, InputDigits } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { Icon, Typography } from '@appwrite.io/pink-svelte';
import { IconChatAlt, IconDeviceMobile, IconMail } from '@appwrite.io/pink-icons-svelte';
export let factors: Models.MfaFactors & { recoveryCode: boolean };
export let showVerifyButton: boolean = true;
export let disabled: boolean = false;
export let challenge: Models.MfaChallenge;
export let code: string;
let challengeType: AuthenticationFactor;
// Get enabled non-recovery factors for main factors
$: enabledMainFactors = [
factors.totp && 'totp',
factors.email && 'email',
factors.phone && 'phone'
].filter(Boolean);
async function createChallenge(factor: AuthenticationFactor) {
disabled = true;
challengeType = factor;
code = '';
try {
if (
factor !== AuthenticationFactor.Totp &&
factor !== AuthenticationFactor.Recoverycode
) {
challenge = await sdk.forConsole.account.createMFAChallenge({ factor });
}
} catch (error) {
addNotification({
type: 'error',
message: error.message
});
} finally {
disabled = false;
}
}
export async function verifyCurrent() {
try {
return await verify(challenge, code, challengeType, factors);
} catch (error) {
// Clear input fields on verification failure
code = '';
throw error;
}
}
onMount(async () => {
if (enabledMainFactors.length === 0) {
return;
}
if (factors.totp) {
challengeType = AuthenticationFactor.Totp;
} else if (factors.email) {
await createChallenge(AuthenticationFactor.Email);
} else if (factors.phone) {
await createChallenge(AuthenticationFactor.Phone);
}
});
</script>
{#if enabledMainFactors.length === 0}
<!-- Skip MFA completely if no factors enabled -->
{:else if challengeType === AuthenticationFactor.Recoverycode}
<Typography.Text align="center">
Enter one of the recovery codes you received when enabling MFA.
</Typography.Text>
<InputText id="recovery-code" bind:value={code} required autofocus />
{:else if challengeType === AuthenticationFactor.Totp}
<Typography.Text align="center">
Enter a 6-digit one-time code from your authenticator app.
</Typography.Text>
<InputDigits bind:value={code} required autofocus />
{:else if challengeType === AuthenticationFactor.Email}
<Typography.Text align="center">
A 6-digit verification code was sent to your email. Enter it below.
</Typography.Text>
<InputDigits bind:value={code} required autofocus />
{:else if challengeType === AuthenticationFactor.Phone}
<Typography.Text align="center">
A 6-digit verification code was sent to your phone. Enter it below.
</Typography.Text>
<InputDigits bind:value={code} required autofocus />
{/if}
{#if showVerifyButton && enabledMainFactors.length > 0}
<Button fullWidth submit {disabled}>Verify</Button>
{/if}
{#if enabledMainFactors.length > 0}
{@const hasSecondaryOptions =
enabledMainFactors.length > 1 ||
(factors.recoveryCode && challengeType !== AuthenticationFactor.Recoverycode) ||
(enabledMainFactors.length === 1 && factors.recoveryCode)}
{#if hasSecondaryOptions}
<span class="with-separators eyebrow-heading-3">or</span>
<div class="u-flex-vertical u-gap-8">
<!-- Show non-active main factors -->
{#if factors.totp && challengeType !== AuthenticationFactor.Totp}
<Button
secondary
fullWidth
{disabled}
on:click={() => createChallenge(AuthenticationFactor.Totp)}>
<Icon icon={IconDeviceMobile} slot="start" size="s" />
Authenticator app
</Button>
{/if}
{#if factors.email && challengeType !== AuthenticationFactor.Email}
<Button
secondary
fullWidth
{disabled}
on:click={() => createChallenge(AuthenticationFactor.Email)}>
<Icon icon={IconMail} slot="start" size="s" />
Email verification
</Button>
{/if}
{#if factors.phone && challengeType !== AuthenticationFactor.Phone}
<Button
secondary
fullWidth
{disabled}
on:click={() => createChallenge(AuthenticationFactor.Phone)}>
<Icon icon={IconChatAlt} slot="start" size="s" />
Phone verification
</Button>
{/if}
{#if factors.recoveryCode && challengeType !== AuthenticationFactor.Recoverycode}
<Button
text
fullWidth
{disabled}
on:click={() => createChallenge(AuthenticationFactor.Recoverycode)}>
<span class="text">Use recovery code</span>
</Button>
{/if}
</div>
{/if}
{/if}