TypeScript 97.5%
SQL 1.4%
Python 0.8%
1"use client";2import * as React from "react";3import { LogOut, MonitorSmartphone } from "lucide-react";4import { authClient, useSession } from "@/lib/auth-client";5import { recordAuditFromClient } from "@/actions/account";6import { Button } from "@/components/ui/button";7import { Badge } from "@/components/ui/badge";8import { Alert } from "@/components/ui/alert";9import { Skeleton } from "@/components/ui/skeleton";10import { EmptyState } from "@/components/ui/empty-state";11import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";12import { formatDate, timeAgo, truncate } from "@/lib/format";1314interface SessionRow {15 id: string;16 token: string;17 ipAddress?: string | null;18 userAgent?: string | null;19 createdAt: Date | string;20 expiresAt: Date | string;21}2223/** Turn a UA string into something a human can recognise, without a UA-parser dependency. */24function describeAgent(ua: string | null | undefined): string {25 if (!ua) return "Unknown device";26 const browser = /Edg\//.test(ua) ? "Edge" : /OPR\//.test(ua) ? "Opera" : /Chrome\//.test(ua) ? "Chrome" : /Firefox\//.test(ua) ? "Firefox" : /Safari\//.test(ua) ? "Safari" : /curl|python|node|okhttp/i.test(ua) ? "API client" : "Browser";27 const os = /iPhone|iPad/.test(ua) ? "iOS" : /Android/.test(ua) ? "Android" : /Mac OS X/.test(ua) ? "macOS" : /Windows/.test(ua) ? "Windows" : /Linux/.test(ua) ? "Linux" : "";28 return os ? `${browser} · ${os}` : browser;29}3031export function ActiveSessions() {32 const { data: current } = useSession();33 const [sessions, setSessions] = React.useState<SessionRow[] | null>(null);34 const [error, setError] = React.useState<string | null>(null);35 const [busyToken, setBusyToken] = React.useState<string | null>(null);36 const [busyAll, setBusyAll] = React.useState(false);3738 const [reloadKey, setReloadKey] = React.useState(0);39 const load = () => setReloadKey((k) => k + 1);4041 React.useEffect(() => {42 let cancelled = false;43 authClient.listSessions().then(({ data, error }) => {44 if (cancelled) return;45 if (error) {46 setError(error.message ?? "Could not load sessions.");47 setSessions([]);48 return;49 }50 const rows = ((data ?? []) as SessionRow[]).slice().sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime());51 setSessions(rows);52 });53 return () => {54 cancelled = true;55 };56 }, [reloadKey]);5758 const currentToken = current?.session?.token;5960 async function revoke(token: string) {61 setError(null);62 setBusyToken(token);63 const { error } = await authClient.revokeSession({ token });64 if (error) setError(error.message ?? "Could not revoke the session.");65 else await recordAuditFromClient("session.revoked", { scope: "one" }).catch(() => {});66 setBusyToken(null);67 load();68 }6970 async function revokeOthers() {71 setError(null);72 setBusyAll(true);73 const { error } = await authClient.revokeOtherSessions();74 if (error) setError(error.message ?? "Could not sign out other devices.");75 else await recordAuditFromClient("session.revoked", { scope: "others" }).catch(() => {});76 setBusyAll(false);77 load();78 }7980 const others = sessions?.filter((s) => s.token !== currentToken).length ?? 0;8182 return (83 <div className="grid gap-3">84 {error ? <Alert variant="danger">{error}</Alert> : null}85 <div className="flex items-center justify-between gap-3">86 <p className="text-[13px] text-fg-muted">87 {sessions ? (88 <>89 <span className="font-mono tabular text-fg">{sessions.length}</span> active {sessions.length === 1 ? "session" : "sessions"}90 {others > 0 ? <> · {others} on other devices</> : null}91 </>92 ) : (93 "Loading sessions…"94 )}95 </p>96 <Button variant="outline" size="sm" onClick={revokeOthers} loading={busyAll} disabled={!sessions || others === 0}>97 <LogOut /> Sign out other devices98 </Button>99 </div>100 {sessions === null ? (101 <div className="grid gap-2 rounded-lg border border-border p-4">102 <Skeleton className="h-4 w-2/3" />103 <Skeleton className="h-4 w-1/2" />104 <Skeleton className="h-4 w-3/5" />105 </div>106 ) : sessions.length === 0 ? (107 <EmptyState compact icon={MonitorSmartphone} title="No sessions found" description="Sign in again to see your devices here." />108 ) : (109 <div className="overflow-hidden rounded-lg border border-border bg-bg-elevated shadow-xs">110 <Table>111 <TableHeader>112 <TableRow className="hover:bg-transparent">113 <TableHead>Device</TableHead>114 <TableHead>IP address</TableHead>115 <TableHead>Signed in</TableHead>116 <TableHead>Expires</TableHead>117 <TableHead className="text-right">118 <span className="sr-only">Actions</span>119 </TableHead>120 </TableRow>121 </TableHeader>122 <TableBody>123 {sessions.map((s) => {124 const isCurrent = s.token === currentToken;125 return (126 <TableRow key={s.id}>127 <TableCell>128 <div className="flex flex-col gap-0.5">129 <span className="flex items-center gap-2 font-medium">130 {describeAgent(s.userAgent)}131 {isCurrent ? (132 <Badge variant="accent" dot>133 This device134 </Badge>135 ) : null}136 </span>137 <span className="font-mono text-[11px] text-fg-subtle" title={s.userAgent ?? undefined}>138 {s.userAgent ? truncate(s.userAgent, 64) : "—"}139 </span>140 </div>141 </TableCell>142 <TableCell className="font-mono text-[12.5px] tabular">{s.ipAddress || "—"}</TableCell>143 <TableCell className="whitespace-nowrap tabular" title={formatDate(s.createdAt)}>144 {timeAgo(s.createdAt)}145 </TableCell>146 <TableCell className="whitespace-nowrap tabular text-fg-muted">{formatDate(s.expiresAt, { timeStyle: undefined })}</TableCell>147 <TableCell className="text-right">148 {!isCurrent ? (149 <Button variant="ghost" size="xs" onClick={() => revoke(s.token)} loading={busyToken === s.token}>150 Revoke151 </Button>152 ) : null}153 </TableCell>154 </TableRow>155 );156 })}157 </TableBody>158 </Table>159 </div>160 )}161 </div>162 );163}164