Files
illusory-mapp/apps/main/src/routes/auth/2fa/+page.svelte
2026-02-28 14:50:04 +02:00

286 lines
7.6 KiB
Svelte

<script lang="ts">
import { authClient } from "$lib/auth.client";
import ButtonText from "$lib/components/atoms/button-text.svelte";
import Icon from "$lib/components/atoms/icon.svelte";
import Title from "$lib/components/atoms/title.svelte";
import Button from "$lib/components/ui/button/button.svelte";
import * as Card from "$lib/components/ui/card/index.js";
import * as InputOTP from "$lib/components/ui/input-otp";
import { twoFactorVerifyVM } from "$lib/domains/security/2fa-verify.vm.svelte";
import { session, user } from "$lib/global.stores";
import {
AlertTriangle,
ArrowLeft,
Key,
Loader2,
Shield,
Smartphone,
XCircle,
} from "@lucide/svelte";
import { onMount } from "svelte";
import type { PageData } from "./$types";
let { data }: { data: PageData } = $props();
let mounted = $state(false);
onMount(async () => {
if (data.user) {
user.set(data.user);
}
if (data.session) {
session.set(data.session);
}
setTimeout(async () => {
mounted = true;
await twoFactorVerifyVM.startVerification();
}, 500);
});
async function handleSubmit() {
await twoFactorVerifyVM.verifyCode();
}
function handleTryAgain() {
twoFactorVerifyVM.reset();
twoFactorVerifyVM.startVerification();
}
function handleSignOut() {
authClient.signOut();
window.location.href = "/auth/login";
}
</script>
<main class="grid h-screen w-screen place-items-center overflow-y-hidden">
<div class="w-full max-w-lg">
<Card.Root class="bg-card/95 border shadow-2xl backdrop-blur-sm">
{#if !mounted || twoFactorVerifyVM.startingVerification}
<!-- Loading State -->
<Card.Content class="p-8">
<div class="flex flex-col items-center space-y-6 text-center">
<div class="relative">
<div
class="bg-primary/20 absolute inset-0 animate-pulse rounded-full blur-xl"
></div>
<div
class="bg-primary/10 border-primary/20 relative rounded-full border p-4"
>
<Icon icon={Shield} cls="h-8 w-8 text-primary" />
</div>
</div>
<div class="space-y-4">
<Title
size="h4"
weight="semibold"
color="theme"
center
>
Preparing Verification
</Title>
<div
class="text-muted-foreground flex items-center justify-center space-x-2"
>
<Icon icon={Loader2} cls="h-5 w-5 animate-spin" />
<span class="text-sm"
>Setting up secure verification...</span
>
</div>
<!-- Progress Animation -->
<div
class="bg-muted/30 h-2 w-full overflow-hidden rounded-full"
>
<div
class="bg-primary from-primary/50 to-primary h-full animate-pulse rounded-full bg-gradient-to-r transition-all duration-1000 ease-in-out"
></div>
</div>
</div>
</div>
</Card.Content>
{:else if twoFactorVerifyVM.verificationToken}
<!-- Verification Form -->
<Card.Header>
<div class="flex items-center justify-center space-x-2 py-2">
<Icon icon={Smartphone} cls="h-6 w-6 text-primary" />
<Card.Title>
<Title size="h4" weight="medium" center>
Two-Factor Authentication
</Title>
</Card.Title>
</div>
<Card.Description class="text-center">
Enter the 6-digit code from your authenticator app to
continue
</Card.Description>
</Card.Header>
<Card.Content
class="flex w-full flex-col items-center justify-center gap-6 p-8"
>
<!-- 2FA Input -->
<div class="w-full max-w-sm space-y-4">
<div class="flex justify-center">
<InputOTP.Root
maxlength={6}
bind:value={twoFactorVerifyVM.verificationCode}
>
{#snippet children({ cells })}
<InputOTP.Group>
{#each cells.slice(0, 3) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
<InputOTP.Separator />
<InputOTP.Group>
{#each cells.slice(3, 6) as cell (cell)}
<InputOTP.Slot {cell} />
{/each}
</InputOTP.Group>
{/snippet}
</InputOTP.Root>
</div>
{#if twoFactorVerifyVM.errorMessage}
<div
class="text-destructive flex items-center justify-center space-x-2 text-sm"
>
<Icon icon={AlertTriangle} cls="h-4 w-4" />
<span>{twoFactorVerifyVM.errorMessage}</span>
</div>
{/if}
<Button
onclick={handleSubmit}
disabled={twoFactorVerifyVM.verifying ||
!twoFactorVerifyVM.verificationCode ||
twoFactorVerifyVM.verificationCode.length !== 6}
class="w-full"
>
<ButtonText
loading={twoFactorVerifyVM.verifying}
text="Verify Code"
loadingText="Verifying..."
/>
</Button>
</div>
<!-- Alternative Options -->
<div class="w-full max-w-sm space-y-3">
<div class="flex w-full items-center gap-4">
<span class="border-muted h-1 w-full border-t-2"
></span>
<p class="text-sm text-zinc-500">OR</p>
<span class="border-muted h-1 w-full border-t-2"
></span>
</div>
<Button
variant="outline"
onclick={() => twoFactorVerifyVM.handleBackupCode()}
class="w-full"
>
<Icon icon={Key} cls="h-4 w-4 mr-2" />
Use Recovery Code
</Button>
</div>
<!-- Troubleshooting -->
<div class="w-full max-w-sm space-y-3 pt-4">
<div class="space-y-2 text-center">
<p class="text-muted-foreground text-xs">
Having trouble?
</p>
<div class="flex justify-center gap-2">
<Button
variant="ghost"
size="sm"
onclick={handleTryAgain}
disabled={twoFactorVerifyVM.verifying}
>
<Icon icon={ArrowLeft} cls="h-3 w-3 mr-1" />
Try Refreshing
</Button>
<Button
variant="ghost"
size="sm"
onclick={handleSignOut}
disabled={twoFactorVerifyVM.verifying}
>
Sign Out
</Button>
</div>
</div>
</div>
<!-- Security Note -->
<div class="border-border w-full border-t pt-4">
<div
class="text-muted-foreground flex items-center justify-center space-x-2 text-xs"
>
<Icon icon={Shield} cls="h-3 w-3" />
<span>
This verification expires in 10 minutes for your
security
</span>
</div>
</div>
</Card.Content>
{:else}
<!-- Error State -->
<Card.Content class="p-8">
<div class="flex flex-col items-center space-y-6 text-center">
<div class="relative w-max">
<div
class="bg-destructive/20 absolute inset-0 rounded-full blur-lg"
></div>
<div
class="bg-destructive/10 border-destructive/20 relative rounded-full border p-4"
>
<Icon
icon={XCircle}
cls="h-8 w-8 text-destructive"
/>
</div>
</div>
<div class="space-y-4">
<Title
size="h4"
weight="semibold"
color="theme"
center
>
Verification Setup Failed
</Title>
<p class="text-muted-foreground text-center text-sm">
{twoFactorVerifyVM.errorMessage ||
"We couldn't set up your verification session. Please try again."}
</p>
<div class="space-y-3 pt-2">
<Button onclick={handleTryAgain} class="w-full">
<Icon icon={ArrowLeft} cls="h-4 w-4 mr-2" />
Try Refreshing
</Button>
<Button
variant="outline"
onclick={handleSignOut}
class="w-full"
>
Sign Out & Start Over
</Button>
</div>
</div>
</div>
</Card.Content>
{/if}
</Card.Root>
</div>
</main>