import { useQuery, useQueryClient } from "@tanstack/react-query"; import { LogOut, Monitor } from "lucide-react"; import { useState } from "react"; import { useNavigate } from "react-router"; import { BackLink } from "@/client/components/ui/back-link"; import { Card } from "@/client/components/ui/card"; import { ConfirmButton } from "@/client/components/ui/confirm-button"; 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 { formatDateTime } from "@/client/lib/format"; import { queryKeys } from "@/client/lib/query"; interface SessionRow { id: string; token: string; ipAddress?: string | null; userAgent?: string | null; createdAt: string | Date; expiresAt: string | Date; } const SessionRowsSkeleton = ({ count }: { count: number }) => (
{Array.from({ length: count }, (_, i) => (
))}
); const AccountSessionsPageSkeleton = () => (
); // Heuristic device label from the User-Agent string. We don't need // perfect parsing — just a recognizable hint so the user can spot // their own device in a list of three. Falls back to the raw UA. const summariseUserAgent = (ua: string | null | undefined): string => { if (!ua) return "Unknown device"; const isMobile = /Mobile|Android|iPhone|iPad/i.test(ua); const browser = /Firefox/i.test(ua) ? "Firefox" : /Edg/i.test(ua) ? "Edge" : /Chrome/i.test(ua) ? "Chrome" : /Safari/i.test(ua) ? "Safari" : "Browser"; const os = /Windows/i.test(ua) ? "Windows" : /Mac OS X/i.test(ua) ? "macOS" : /Android/i.test(ua) ? "Android" : /iPhone|iPad|iOS/i.test(ua) ? "iOS" : /Linux/i.test(ua) ? "Linux" : "device"; return `${browser} on ${os}${isMobile ? " (mobile)" : ""}`; }; export const AccountSessionsPage = () => { const navigate = useNavigate(); const session = useRequireSession("/account/sessions"); const queryClient = useQueryClient(); const [error, setError] = useState(null); const [pending, setPending] = useState(null); const userId = session.data?.user.id ?? null; const sessionsQueryKey = queryKeys.accountSessions(userId); const sessionsQuery = useQuery({ queryKey: sessionsQueryKey, queryFn: async () => { const res = await authClient.listSessions(); if (res.error) throw new Error(res.error.message ?? "Could not load sessions."); return (res.data ?? []) as unknown as SessionRow[]; }, enabled: Boolean(userId), }); const sessions = sessionsQuery.data ?? null; const handleRevoke = async (row: SessionRow) => { setError(null); setPending(row.id); try { const res = await authClient.revokeSession({ token: row.token }); if (res.error) { setError(res.error.message ?? "Could not revoke session."); return; } // Revoking the current session signs us out — bounce to landing. if (row.token === session.data?.session.token) { await session.refetch(); navigate("/"); return; } await queryClient.invalidateQueries({ queryKey: sessionsQueryKey, exact: true }); } catch (e) { setError(e instanceof Error ? e.message : "Failed to revoke session."); } finally { setPending(null); } }; const handleRevokeOthers = async () => { setError(null); setPending("others"); try { const res = await authClient.revokeOtherSessions(); if (res.error) { setError(res.error.message ?? "Could not revoke other sessions."); return; } await queryClient.invalidateQueries({ queryKey: sessionsQueryKey, exact: true }); } catch (e) { setError(e instanceof Error ? e.message : "Failed to revoke other sessions."); } finally { setPending(null); } }; if (session.isPending || !session.data) { return ; } const currentToken = session.data.session.token; const otherCount = sessions?.filter((s) => s.token !== currentToken).length ?? 0; return (
Account setError(null)} />

Active sessions

{sessions && otherCount > 0 ? ( ) : null}
{sessions === null ? ( ) : (
{sessions.map((row) => { const isCurrent = row.token === currentToken; return (
handleRevoke(row)} > Revoke
); })} {sessions.length === 0 ?

No active sessions.

: null}
)}
); };