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:
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user