"use client" import { useState, useEffect } from "react" import { Loader2, Link as LinkIcon, Unlink, Shield } from "lucide-react" import { motion } from "motion/react" import { FaGoogle, FaDiscord } from "react-icons/fa" interface LinkedAccount { id: string providerId: string accountId: string createdAt: string } interface AuthMethods { hasPassword: boolean passkeyCount: number oauthProviders: { providerId: string; id: string }[] totalAuthMethods: number } const providerConfig: Record | null; color: string; bgColor: string }> = { google: { name: "Google", icon: FaGoogle, color: "text-red-400", bgColor: "bg-red-500/10 border-red-500/20" }, discord: { name: "Discord", icon: FaDiscord, color: "text-indigo-400", bgColor: "bg-indigo-500/10 border-indigo-500/20" }, credential: { name: "Password", icon: null, color: "text-text/60", bgColor: "bg-text/5 border-border" }, } export function SettingsAccountsTab() { const [accounts, setAccounts] = useState([]) const [loading, setLoading] = useState(true) const [authMethods, setAuthMethods] = useState(null) const [unlinking, setUnlinking] = useState(null) const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null) const refreshData = async () => { try { const [accountsRes, methodsRes] = await Promise.all([ fetch("/api/auth/list-accounts", { credentials: "include" }), fetch("/api/user/me/auth-methods"), ]) if (accountsRes.ok) { const data = await accountsRes.json() setAccounts(Array.isArray(data) ? data : []) } if (methodsRes.ok) { const data = await methodsRes.json() setAuthMethods(data) } } catch { // silently fail } } useEffect(() => { if (accounts.length > 0 || authMethods !== null) return let cancelled = false Promise.all([ fetch("/api/auth/list-accounts", { credentials: "include" }).then(r => r.ok ? r.json() : []).catch(() => []), fetch("/api/user/me/auth-methods").then(r => r.ok ? r.json() : null).catch(() => null), ]).then(([accountsData, methodsData]) => { if (cancelled) return if (accountsData) setAccounts(Array.isArray(accountsData) ? accountsData : []) if (methodsData) setAuthMethods(methodsData) setLoading(false) }) return () => { cancelled = true } // eslint-disable-next-line react-hooks/exhaustive-deps -- intentional: fetch only on mount }, []) const handleLink = async (provider: "google" | "discord") => { try { const res = await fetch("/api/auth/link-social", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ provider, callbackURL: window.location.href, }), }) if (!res.ok) { const data = await res.json().catch(() => ({})) setMessage({ type: "error", text: data.message || "Failed to link account" }) return } const data = await res.json() // If the API returns a URL, redirect to it if (data && data.url) { window.location.assign(data.url as string) } } catch { setMessage({ type: "error", text: "Failed to initiate account linking" }) } } const handleUnlink = async (providerId: string) => { if (authMethods && authMethods.totalAuthMethods <= 1) return setUnlinking(providerId) setMessage(null) try { const res = await fetch("/api/auth/unlink-account", { method: "POST", headers: { "Content-Type": "application/json" }, credentials: "include", body: JSON.stringify({ providerId }), }) if (res.ok) { setMessage({ type: "success", text: `${providerConfig[providerId]?.name || providerId} account unlinked` }) await refreshData() } else { const data = await res.json() setMessage({ type: "error", text: data.message || "Failed to unlink account" }) } } catch { setMessage({ type: "error", text: "Failed to unlink account" }) } finally { setUnlinking(null) } } const linkedProviders = new Set(accounts.map((a) => a.providerId)) const availableProviders = ["google", "discord"].filter((p) => !linkedProviders.has(p)) const isOnlyAuthMethod = authMethods ? authMethods.totalAuthMethods <= 1 : true return ( {/* Warning if only one auth method */} {isOnlyAuthMethod && (

Single authentication method

You only have one way to sign in. Consider linking a social account or adding a passkey.

)} {message && (

{message.text}

)} {/* Linked Accounts */}

Linked Accounts

{loading ? (
) : (
{/* Password status */} {authMethods && (

Password

{authMethods.hasPassword ? "Configured" : "Not set"}

{authMethods.hasPassword ? "Active" : "Inactive"}
)} {/* OAuth accounts */} {accounts.map((account) => { const config = providerConfig[account.providerId] || { name: account.providerId, icon: null, color: "text-text/60", bgColor: "bg-text/5 border-border", } const canUnlink = !isOnlyAuthMethod return (
{config.icon ? : }

{config.name}

Linked

) })} {/* Passkeys count */} {authMethods && authMethods.passkeyCount > 0 && (

Passkeys

{authMethods.passkeyCount} registered

Active
)}
)}
{/* Link New Account */} {availableProviders.length > 0 && (

Link a Social Account

{availableProviders.map((provider) => { const config = providerConfig[provider] if (!config) return null return ( ) })}
)}
) }