"use client" import { useState, useRef, useEffect } from "react" import { authClient } from "@/lib/auth-client" import { Loader2, Key, Fingerprint, Plus, Trash2, Pencil, Check, X, Shield } from "lucide-react" import { motion, AnimatePresence } from "motion/react" interface Passkey { id: string name: string | null deviceType: string createdAt: string | null } interface AuthMethods { hasPassword: boolean passkeyCount: number oauthProviders: Array<{ providerId: string; id: string }> totalAuthMethods: number } export function SettingsSecurityTab() { const [authMethods, setAuthMethods] = useState(null) const [passkeys, setPasskeys] = useState([]) const [isLoadingAuthMethods, setIsLoadingAuthMethods] = useState(true) const [isLoadingPasskeys, setIsLoadingPasskeys] = useState(true) const [passwordMessage, setPasswordMessage] = useState<{ type: "success" | "error"; text: string } | null>(null) const [isPasswordSubmitting, setIsPasswordSubmitting] = useState(false) // Password form state const [currentPassword, setCurrentPassword] = useState("") const [newPassword, setNewPassword] = useState("") const [confirmPassword, setConfirmPassword] = useState("") // Passkey editing state const [editingPasskeyId, setEditingPasskeyId] = useState(null) const [editingName, setEditingName] = useState("") const [isUpdatingPasskey, setIsUpdatingPasskey] = useState(false) const [isDeletingPasskey, setIsDeletingPasskey] = useState(null) const [isAddingPasskey, setIsAddingPasskey] = useState(false) const fetchRef = useRef(false) useEffect(() => { if (fetchRef.current) return fetchRef.current = true let cancelled = false Promise.all([ fetch("/api/user/me/auth-methods", { credentials: "include" }).then(r => r.ok ? r.json() : null).catch(() => null), fetch("/api/auth/passkey/list", { credentials: "include" }).then(r => r.ok ? r.json() : []).catch(() => []), ]).then(([methodsData, passkeyData]) => { if (cancelled) return if (methodsData) setAuthMethods(methodsData) if (passkeyData) setPasskeys(Array.isArray(passkeyData) ? passkeyData : []) setIsLoadingAuthMethods(false) setIsLoadingPasskeys(false) }) return () => { cancelled = true } }, []) const refreshAuthMethods = async () => { const res = await fetch("/api/user/me/auth-methods", { credentials: "include" }) if (res.ok) setAuthMethods(await res.json()) } const refreshPasskeys = async () => { const res = await fetch("/api/auth/passkey/list", { credentials: "include" }) if (res.ok) { const data = await res.json() setPasskeys(Array.isArray(data) ? data : []) } } const handlePasswordSubmit = async (e: React.FormEvent) => { e.preventDefault() setPasswordMessage(null) if (newPassword !== confirmPassword) { setPasswordMessage({ type: "error", text: "Passwords do not match" }) return } if (newPassword.length < 10) { setPasswordMessage({ type: "error", text: "Password must be at least 10 characters" }) return } setIsPasswordSubmitting(true) if (authMethods?.hasPassword) { const { error } = await authClient.changePassword({ currentPassword, newPassword, }) if (error) { setPasswordMessage({ type: "error", text: error.message || "Failed to change password" }) } else { setPasswordMessage({ type: "success", text: "Password changed successfully" }) setCurrentPassword("") setNewPassword("") setConfirmPassword("") } } else { const res = await fetch("/api/user/me/set-password", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ newPassword }), credentials: "include", }) if (!res.ok) { const data = await res.json().catch(() => ({ error: "Failed to set password" })) setPasswordMessage({ type: "error", text: data.error || "Failed to set password" }) } else { setPasswordMessage({ type: "success", text: "Password set successfully" }) setNewPassword("") setConfirmPassword("") await refreshAuthMethods() } } setIsPasswordSubmitting(false) } const handleAddPasskey = async () => { setIsAddingPasskey(true) try { const { error } = await authClient.passkey.addPasskey() if (error) { console.error("Failed to add passkey:", error) } else { await refreshPasskeys() await refreshAuthMethods() } } catch { // silently fail } finally { setIsAddingPasskey(false) } } const handleDeletePasskey = async (id: string) => { if (authMethods && authMethods.totalAuthMethods <= 1) return setIsDeletingPasskey(id) try { const res = await fetch("/api/user/me/passkey/delete", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id }), credentials: "include", }) if (res.ok) { await refreshPasskeys() await refreshAuthMethods() } } catch { // silently fail } finally { setIsDeletingPasskey(null) } } const startEditingPasskey = (passkey: Passkey) => { setEditingPasskeyId(passkey.id) setEditingName(passkey.name || "") } const cancelEditingPasskey = () => { setEditingPasskeyId(null) setEditingName("") } const handleRenamePasskey = async (id: string) => { setIsUpdatingPasskey(true) try { const res = await fetch("/api/user/me/passkey/update", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id, name: editingName }), credentials: "include", }) if (res.ok) { await refreshPasskeys() setEditingPasskeyId(null) setEditingName("") } } catch { // silently fail } finally { setIsUpdatingPasskey(false) } } const isSingleAuthMethod = authMethods && authMethods.totalAuthMethods <= 1 return ( {/* Warning for single auth method */} {isSingleAuthMethod && (

Single authentication method — consider adding a passkey or linking a social account

)}
{/* Password Section */}

Password

{isLoadingAuthMethods ? (
Loading...
) : (
{authMethods?.hasPassword && (
setCurrentPassword(e.target.value)} className="w-full px-3 py-2 rounded-lg bg-text/5 border border-border text-sm text-text focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/30 transition-colors" placeholder="Enter current password" required />
)}
setNewPassword(e.target.value)} className="w-full px-3 py-2 rounded-lg bg-text/5 border border-border text-sm text-text focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/30 transition-colors" placeholder={authMethods?.hasPassword ? "Enter new password" : "Set a password"} required />
setConfirmPassword(e.target.value)} className="w-full px-3 py-2 rounded-lg bg-text/5 border border-border text-sm text-text focus:outline-none focus:border-primary/60 focus:ring-1 focus:ring-primary/30 transition-colors" placeholder="Confirm password" required />
{passwordMessage && (

{passwordMessage.text}

)}
)}
{/* Passkey Section */}

Passkeys

{isLoadingPasskeys ? (
Loading passkeys...
) : passkeys.length === 0 ? (

No passkeys registered.

) : (
{passkeys.map((pk) => (
{editingPasskeyId === pk.id ? (
setEditingName(e.target.value)} className="px-2 py-1 rounded bg-text/5 border border-border text-sm text-text focus:outline-none focus:border-primary/60" autoFocus />
) : ( <>

{pk.name || "Unnamed passkey"}

{pk.deviceType} {pk.createdAt && ( · Added {new Date(pk.createdAt).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric", })} )}

)}
{editingPasskeyId !== pk.id && (
)}
))}
)}
) }