286 lines
7.6 KiB
Svelte
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>
|