diff --git a/app/auth/login/page.tsx b/app/auth/login/page.tsx index 2c95e9d..452a0fa 100644 --- a/app/auth/login/page.tsx +++ b/app/auth/login/page.tsx @@ -7,7 +7,8 @@ import { Label } from '@/components/ui/label' import Link from 'next/link' import { useRouter } from 'next/navigation' import { useState } from 'react' -import { Mail, Lock, Loader2, ArrowLeft } from 'lucide-react' +import { Mail, Lock, Loader2, ArrowLeft, Key } from 'lucide-react' +import { isPasskeySupported, authenticateWithPasskey } from '@/lib/passkeys' export default function LoginPage() { const [email, setEmail] = useState('') @@ -15,6 +16,8 @@ export default function LoginPage() { const [error, setError] = useState(null) const [isLoading, setIsLoading] = useState(false) const [isGoogleLoading, setIsGoogleLoading] = useState(false) + const [isPasskeyLoading, setIsPasskeyLoading] = useState(false) + const [passkeySupported] = useState(() => isPasskeySupported()) const router = useRouter() const handleLogin = async (e: React.FormEvent) => { @@ -56,6 +59,33 @@ export default function LoginPage() { } } + const handlePasskeyLogin = async () => { + const supabase = createClient() + setIsPasskeyLoading(true) + setError(null) + + try { + const credential = await authenticateWithPasskey() + + if (!credential) { + throw new Error('Passkey authentication failed') + } + + // For now, passkeys are registered as a factor in Supabase + // This is a placeholder - in production, you'd verify the credential server-side + console.log('[v0] Passkey authentication initiated:', credential.id) + + // Since this is invite-only, users would have pre-registered their passkeys + // and we'd verify them here. For now, show success. + setError('Passkey authentication requires server-side verification setup') + } catch (error: unknown) { + const message = error instanceof Error ? error.message : 'Passkey authentication failed' + setError(message) + } finally { + setIsPasskeyLoading(false) + } + } + return (
{/* Background elements */} @@ -89,6 +119,26 @@ export default function LoginPage() {

+ {/* Passkey Button */} + {passkeySupported && ( + + )} + {/* Google OAuth Button */} + + + {error && ( +
+

{error}

+
+ )} + + {success && ( +
+

{success}

+
+ )} + + {passkeys.length === 0 ? ( +
+ +

+ No passkeys registered yet. Add one to enable passwordless sign-in. +

+
+ ) : ( +
+ {passkeys.map((passkey) => ( +
+
+

{passkey.name}

+

+ Added on {passkey.createdAt} +

+
+ +
+ ))} +
+ )} + + ) +} diff --git a/lib/passkeys.ts b/lib/passkeys.ts new file mode 100644 index 0000000..219ee6e --- /dev/null +++ b/lib/passkeys.ts @@ -0,0 +1,83 @@ +'use client' + +import { createClient } from '@/lib/supabase/client' + +export async function registerPasskey(userId: string, displayName: string) { + const supabase = createClient() + + if (!window.PublicKeyCredential) { + throw new Error('Passkeys are not supported in your browser') + } + + try { + // Get registration options from Supabase + const { data, error } = await supabase.auth.signInWithEnrollFactors({ + factorType: 'totp', + }) + + if (error) throw error + + // For now, we'll use a simplified approach with browser's WebAuthn API + // Create a passkey credential + const credential = await navigator.credentials.create({ + publicKey: { + challenge: new Uint8Array(32), + rp: { + name: 'Cloudrite', + id: window.location.hostname, + }, + user: { + id: new TextEncoder().encode(userId), + name: userId, + displayName: displayName, + }, + pubKeyCredParams: [{ type: 'public-key', alg: -7 }], + timeout: 60000, + attestation: 'direct', + }, + }) as PublicKeyCredential | null + + if (!credential) { + throw new Error('Failed to create passkey') + } + + return credential + } catch (error) { + console.error('[v0] Passkey registration failed:', error) + throw error + } +} + +export async function authenticateWithPasskey() { + if (!window.PublicKeyCredential) { + throw new Error('Passkeys are not supported in your browser') + } + + try { + const assertion = await navigator.credentials.get({ + publicKey: { + challenge: new Uint8Array(32), + timeout: 60000, + userVerification: 'preferred', + }, + }) as PublicKeyCredential | null + + if (!assertion) { + throw new Error('Passkey authentication cancelled or failed') + } + + return assertion + } catch (error) { + console.error('[v0] Passkey authentication failed:', error) + throw error + } +} + +export function isPasskeySupported(): boolean { + return !!( + window.PublicKeyCredential && + navigator.credentials && + navigator.credentials.create && + navigator.credentials.get + ) +}