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 ? (
Sign out everywhere else
) : null}
{sessions === null ? (
) : (
{sessions.map((row) => {
const isCurrent = row.token === currentToken;
return (
{summariseUserAgent(row.userAgent)}
{isCurrent ? (
this device
) : null}
{row.ipAddress ?? "ip unknown"} · started {formatDateTime(row.createdAt)}
handleRevoke(row)}
>
Revoke
);
})}
{sessions.length === 0 ?
No active sessions.
: null}
)}
);
};