feat: implement passkey support with Supabase auth

Add WebAuthn utility functions and passkey manager component; update login and dashboard pages

Co-authored-by: ASOwnerYT <[email protected]>
This commit is contained in:
v0
2026-03-22 23:35:12 +00:00
co-authored by ASOwnerYT
parent da5a2e6aab
commit 1f5631552c
4 changed files with 303 additions and 24 deletions
+137
View File
@@ -0,0 +1,137 @@
'use client'
import { useState } from 'react'
import { Button } from '@/components/ui/button'
import { Key, Plus, Trash2, Loader2, AlertCircle } from 'lucide-react'
import { registerPasskey, isPasskeySupported } from '@/lib/passkeys'
export function PasskeyManager() {
const [isRegistering, setIsRegistering] = useState(false)
const [error, setError] = useState<string | null>(null)
const [success, setSuccess] = useState<string | null>(null)
const [passkeys, setPasskeys] = useState<Array<{ id: string; name: string; createdAt: string }>>([])
const passkeySupported = isPasskeySupported()
const handleRegisterPasskey = async () => {
if (!passkeySupported) {
setError('Your browser does not support passkeys')
return
}
setIsRegistering(true)
setError(null)
setSuccess(null)
try {
const credential = await registerPasskey('', 'My Passkey')
// In a real implementation, you'd send the credential to your server
// and store it securely associated with the user
const newPasskey = {
id: credential.id,
name: 'My Passkey',
createdAt: new Date().toLocaleDateString(),
}
setPasskeys([...passkeys, newPasskey])
setSuccess('Passkey registered successfully')
// Clear success message after 5 seconds
setTimeout(() => setSuccess(null), 5000)
} catch (error: unknown) {
const message = error instanceof Error ? error.message : 'Failed to register passkey'
setError(message)
} finally {
setIsRegistering(false)
}
}
const handleDeletePasskey = (id: string) => {
setPasskeys(passkeys.filter(pk => pk.id !== id))
setSuccess('Passkey removed')
setTimeout(() => setSuccess(null), 5000)
}
if (!passkeySupported) {
return (
<div className="p-6 bg-card border border-border rounded-xl">
<div className="flex items-center gap-3 text-muted-foreground mb-2">
<AlertCircle className="w-5 h-5" />
<h3 className="font-semibold">Passkey Support Not Available</h3>
</div>
<p className="text-sm text-muted-foreground">
Your browser does not support passkeys. Please use a modern browser like Chrome, Safari, or Edge.
</p>
</div>
)
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Key className="w-5 h-5 text-primary" />
<h3 className="font-semibold">Passkeys</h3>
</div>
<Button
size="sm"
onClick={handleRegisterPasskey}
disabled={isRegistering}
className="bg-primary hover:bg-primary/90"
>
{isRegistering ? (
<Loader2 className="w-4 h-4 animate-spin mr-2" />
) : (
<Plus className="w-4 h-4 mr-2" />
)}
Add Passkey
</Button>
</div>
{error && (
<div className="p-3 bg-destructive/10 border border-destructive/50 rounded-lg">
<p className="text-sm text-destructive">{error}</p>
</div>
)}
{success && (
<div className="p-3 bg-primary/10 border border-primary/50 rounded-lg">
<p className="text-sm text-primary">{success}</p>
</div>
)}
{passkeys.length === 0 ? (
<div className="p-6 bg-card border border-border rounded-xl text-center">
<Key className="w-12 h-12 text-muted-foreground mx-auto mb-3 opacity-50" />
<p className="text-muted-foreground text-sm">
No passkeys registered yet. Add one to enable passwordless sign-in.
</p>
</div>
) : (
<div className="space-y-2">
{passkeys.map((passkey) => (
<div
key={passkey.id}
className="flex items-center justify-between p-4 bg-card border border-border rounded-lg"
>
<div>
<p className="font-medium">{passkey.name}</p>
<p className="text-xs text-muted-foreground">
Added on {passkey.createdAt}
</p>
</div>
<Button
size="sm"
variant="ghost"
onClick={() => handleDeletePasskey(passkey.id)}
className="text-destructive hover:text-destructive/90"
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
))}
</div>
)}
</div>
)
}