"use client" import { useState, 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 } interface SettingsSecurityTabProps { authMethods: AuthMethods | null isLoadingAuthMethods: boolean onRefreshAuthMethods: () => Promise } export function SettingsSecurityTab({ authMethods, isLoadingAuthMethods, onRefreshAuthMethods, }: SettingsSecurityTabProps) { const [passkeys, setPasskeys] = useState([]) const [isLoadingPasskeys, setIsLoadingPasskeys] = useState(true) const [passkeysError, setPasskeysError] = useState(null) 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 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 [passkeyMessage, setPasskeyMessage] = useState<{ type: "success" | "error" text: string } | null>(null) useEffect(() => { fetchPasskeys() }, []) async function fetchPasskeys() { try { const res = await fetch("/api/auth/passkey/list-user-passkeys", { credentials: "include", }) if (!res.ok) { setPasskeysError("Failed to load passkeys") setPasskeys([]) } else { const data = await res.json() setPasskeys(Array.isArray(data) ? data : []) setPasskeysError(null) } } catch { setPasskeysError("Failed to load passkeys. Please try again.") setPasskeys([]) } finally { setIsLoadingPasskeys(false) } } const refreshPasskeys = async () => { try { const res = await fetch("/api/auth/passkey/list-user-passkeys", { credentials: "include", }) if (res.ok) { const data = await res.json() setPasskeys(Array.isArray(data) ? data : []) setPasskeysError(null) } } catch { // silently fail on refresh } } 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) try { 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("") await onRefreshAuthMethods() } } 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 onRefreshAuthMethods() } } } catch { setPasswordMessage({ type: "error", text: "An unexpected error occurred", }) } finally { setIsPasswordSubmitting(false) } } const handleAddPasskey = async () => { setIsAddingPasskey(true) setPasskeyMessage(null) try { const { error } = await authClient.passkey.addPasskey() if (error) { setPasskeyMessage({ type: "error", text: error.message || "Failed to add passkey", }) } else { setPasskeyMessage({ type: "success", text: "Passkey added successfully", }) await refreshPasskeys() await onRefreshAuthMethods() } } catch { setPasskeyMessage({ type: "error", text: "Failed to add passkey. Please try again.", }) } finally { setIsAddingPasskey(false) } } const handleDeletePasskey = async (id: string) => { if (authMethods && authMethods.totalAuthMethods <= 1) return setIsDeletingPasskey(id) setPasskeyMessage(null) try { const res = await fetch("/api/auth/passkey/delete-passkey", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ id }), }) if (!res.ok) { const data = await res .json() .catch(() => ({ message: "Failed to delete passkey" })) setPasskeyMessage({ type: "error", text: data.message || "Failed to delete passkey", }) } else { setPasskeyMessage({ type: "success", text: "Passkey deleted" }) await refreshPasskeys() await onRefreshAuthMethods() } } catch { setPasskeyMessage({ type: "error", text: "Failed to delete passkey. Please try again.", }) } finally { setIsDeletingPasskey(null) } } const handleRenamePasskey = async (id: string) => { setIsUpdatingPasskey(true) setPasskeyMessage(null) try { const res = await fetch("/api/auth/passkey/update-passkey", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ id, name: editingName }), }) if (!res.ok) { const data = await res .json() .catch(() => ({ message: "Failed to rename passkey" })) setPasskeyMessage({ type: "error", text: data.message || "Failed to rename passkey", }) } else { setPasskeyMessage({ type: "success", text: "Passkey renamed" }) await refreshPasskeys() setEditingPasskeyId(null) setEditingName("") } } catch { setPasskeyMessage({ type: "error", text: "Failed to rename passkey. Please try again.", }) } finally { setIsUpdatingPasskey(false) } } const startEditingPasskey = (passkey: Passkey) => { setEditingPasskeyId(passkey.id) setEditingName(passkey.name || "") } const cancelEditingPasskey = () => { setEditingPasskeyId(null) setEditingName("") } 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 === null ? (
Failed to load authentication methods.{' '}
) : (
{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

{passkeyMessage && (
{passkeyMessage.text}
)} {passkeysError ? (

{passkeysError}

) : 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 && (
)}
))}
)}
) }