import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Github, Unlink } from "lucide-react"; import { useState } from "react"; import { BackLink } from "@/client/components/ui/back-link"; import { Button } from "@/client/components/ui/button"; import { Card } from "@/client/components/ui/card"; import { ConfirmButton } from "@/client/components/ui/confirm-button"; import { GoogleIcon } from "@/client/components/ui/google-icon"; import { PageError } from "@/client/components/ui/page-error"; import { PageHeader } from "@/client/components/ui/page-header"; import { PanelRow } from "@/client/components/ui/row"; import { Skeleton } from "@/client/components/ui/skeleton"; import { useRequireSession } from "@/client/hooks/use-auth-guard"; import { authClient } from "@/client/lib/auth-client"; import { type SocialProvider, useClientConfig } from "@/client/lib/config"; import { formatDate } from "@/client/lib/format"; import { queryKeys } from "@/client/lib/query"; interface LinkedAccount { id: string; providerId: string; accountId: string; createdAt: string | Date; } const PROVIDER_LABEL: Record = { credential: "Email + password", github: "GitHub", google: "Google", }; const IdentityRowsSkeleton = ({ count }: { count: number }) => (
{Array.from({ length: count }, (_, i) => (
))}
); const AccountIdentitiesPageSkeleton = () => (
); export const AccountIdentitiesPage = () => { const session = useRequireSession("/account/identities"); const queryClient = useQueryClient(); const [error, setError] = useState(null); const [pending, setPending] = useState(null); const config = useClientConfig(); const socialProviders: SocialProvider[] = config?.status === "ok" ? config.socialProviders : []; const userId = session.data?.user.id ?? null; const accountsQueryKey = queryKeys.accountIdentities(userId); const accountsQuery = useQuery({ queryKey: accountsQueryKey, queryFn: async () => { const res = await authClient.listAccounts(); if (res.error) throw new Error(res.error.message ?? "Could not load identities."); return (res.data ?? []) as unknown as LinkedAccount[]; }, enabled: Boolean(userId), }); const accounts = accountsQuery.data ?? null; const credentialCount = accounts?.filter((a) => a.providerId === "credential").length ?? 0; const totalAccounts = accounts?.length ?? 0; const linked = (provider: string) => Boolean(accounts?.find((a) => a.providerId === provider)); const handleLink = async (provider: "github" | "google") => { setError(null); setPending(provider); try { const res = await authClient.linkSocial({ provider, callbackURL: "/account/identities" }); if (res.error) { setError( res.error.message ?? `Could not link ${provider}. The provider may not be configured on this server — ask your operator.`, ); setPending(null); return; } if (res.data?.url) { window.location.assign(res.data.url); return; } setError(`Could not link ${provider}: no redirect URL returned.`); setPending(null); } catch (e) { setError(e instanceof Error ? e.message : `Failed to link ${provider}.`); setPending(null); } }; const handleUnlink = async (account: LinkedAccount) => { setError(null); if (account.providerId === "credential" && credentialCount <= 1 && (accounts?.length ?? 0) <= 1) { setError("Cannot unlink your only credential."); return; } setPending(account.id); try { const res = await authClient.unlinkAccount({ accountId: account.id, }); if (res.error) { setError(res.error.message ?? `Could not unlink ${account.providerId}.`); return; } await queryClient.invalidateQueries({ queryKey: accountsQueryKey, exact: true }); } catch (e) { setError(e instanceof Error ? e.message : "Failed to unlink account."); } finally { setPending(null); } }; if (session.isPending || !session.data) { return ; } return (
Account setError(null)} />

Linked identities

{accounts ? (
{socialProviders.includes("github") && !linked("github") ? ( ) : null} {socialProviders.includes("google") && !linked("google") ? ( ) : null}
) : null}
{accounts === null ? ( ) : (
{accounts.map((account) => { // Block the unlink that would leave the user with zero ways // to sign in. That's the credential row when it's the only // credential and there are no socials, OR any social row // when it's the only account on the user. const isLastCredential = account.providerId === "credential" && credentialCount <= 1; const isLastAccount = totalAccounts <= 1; const canUnlink = !isLastCredential && !isLastAccount; return (

{PROVIDER_LABEL[account.providerId] ?? account.providerId}

Linked {formatDate(account.createdAt)}

{canUnlink ? ( handleUnlink(account)} > ) : ( Last sign-in method. )}
); })}
)}
); };