Add WebAuthn utility functions and passkey manager component; update login and dashboard pages Co-authored-by: ASOwnerYT <[email protected]>
138 lines
4.5 KiB
TypeScript
138 lines
4.5 KiB
TypeScript
'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>
|
|
)
|
|
}
|