From 6df13f5356fb863afd399bbd22e323fecf995dfa Mon Sep 17 00:00:00 2001 From: Developing-Gamer Date: Wed, 27 May 2026 12:47:51 -0700 Subject: [PATCH] Add active sessions page for dashboard account settings. Co-authored-by: Cursor --- .../active-sessions/active-sessions-page.tsx | 289 ++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 apps/dashboard/src/components/dashboard-account-settings/active-sessions/active-sessions-page.tsx diff --git a/apps/dashboard/src/components/dashboard-account-settings/active-sessions/active-sessions-page.tsx b/apps/dashboard/src/components/dashboard-account-settings/active-sessions/active-sessions-page.tsx new file mode 100644 index 000000000..345cd2385 --- /dev/null +++ b/apps/dashboard/src/components/dashboard-account-settings/active-sessions/active-sessions-page.tsx @@ -0,0 +1,289 @@ +'use client'; + +import { fromNow } from "@stackframe/stack-shared/dist/utils/dates"; +import { captureError } from "@stackframe/stack-shared/dist/utils/errors"; +import { runAsynchronously } from "@stackframe/stack-shared/dist/utils/promises"; +import { Button } from "@/components/ui/button"; +import { Badge } from "@/components/ui/badge"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { DotsThree, Monitor, DeviceMobile, Warning } from "@phosphor-icons/react"; +import { useEffect, useState } from "react"; +import { useUser } from "@stackframe/stack"; +import { ActiveSession } from "../supporting/types"; +import { PageLayout } from "../page-layout"; +import { cn } from "@/lib/utils"; + +export function ActiveSessionsPage(props?: { + mockSessions?: Array<{ + id: string, + isCurrentSession: boolean, + isImpersonation?: boolean, + createdAt: string, + lastUsedAt?: string, + geoInfo?: { + ip?: string, + cityName?: string, + }, + }>, + mockMode?: boolean, +}) { + const userFromHook = useUser({ or: (props?.mockSessions || props?.mockMode) ? 'return-null' : 'throw' }); + const [isLoading, setIsLoading] = useState(!props?.mockSessions); + const [isRevokingAll, setIsRevokingAll] = useState(false); + const [sessions, setSessions] = useState([]); + const [showConfirmRevokeAll, setShowConfirmRevokeAll] = useState(false); + + // Use mock data if provided + const mockSessionsData = props?.mockSessions ? props.mockSessions.map(session => ({ + id: session.id, + isCurrentSession: session.isCurrentSession, + isImpersonation: session.isImpersonation || false, + createdAt: session.createdAt, + lastUsedAt: session.lastUsedAt, + geoInfo: session.geoInfo, + })) : [ + { + id: 'current-session', + isCurrentSession: true, + createdAt: new Date().toISOString(), + lastUsedAt: new Date().toISOString(), + geoInfo: { ip: '192.168.1.1', cityName: 'San Francisco' } + }, + { + id: 'mobile-session', + isCurrentSession: false, + createdAt: new Date(Date.now() - 86400000).toISOString(), + lastUsedAt: new Date(Date.now() - 7200000).toISOString(), + geoInfo: { ip: '10.0.0.1', cityName: 'New York' } + } + ]; + + useEffect(() => { + if (props?.mockSessions) { + setSessions(mockSessionsData as any); + setIsLoading(false); + return; + } + + if (props?.mockMode && !userFromHook) { + setSessions(mockSessionsData as any); + setIsLoading(false); + return; + } + + if (!userFromHook) return; + + runAsynchronously(async () => { + setIsLoading(true); + const sessionsData = await userFromHook.getActiveSessions(); + setSessions(sessionsData); + setIsLoading(false); + }); + }, [userFromHook, props?.mockSessions]); + + const handleRevokeSession = async (sessionId: string) => { + if (props?.mockSessions) { + setSessions(prev => prev.filter(session => session.id !== sessionId)); + return; + } + + if (!userFromHook) return; + + try { + await userFromHook.revokeSession(sessionId); + setSessions(prev => prev.filter(session => session.id !== sessionId)); + } catch (error) { + captureError("session-revoke", { sessionId, error }); + throw error; + } + }; + + const handleRevokeAllSessions = async () => { + setIsRevokingAll(true); + try { + if (props?.mockSessions) { + setSessions(prevSessions => prevSessions.filter(session => session.isCurrentSession)); + } else if (userFromHook) { + const deletionPromises = sessions + .filter(session => !session.isCurrentSession) + .map(session => userFromHook.revokeSession(session.id)); + await Promise.all(deletionPromises); + setSessions(prevSessions => prevSessions.filter(session => session.isCurrentSession)); + } + } catch (error) { + captureError("all-sessions-revoke", { error, sessionIds: sessions.map(session => session.id) }); + throw error; + } finally { + setIsRevokingAll(false); + setShowConfirmRevokeAll(false); + } + }; + + return ( + +
+
+
+

+ Active Sessions +

+

+ These are devices where you're currently logged in. You can revoke access to end a session. +

+
+ {sessions.filter(s => !s.isCurrentSession).length > 0 && !isLoading && ( + showConfirmRevokeAll ? ( +
+ + +
+ ) : ( + + ) + )} +
+ + {isLoading ? ( +
+ + +
+ ) : ( +
+ + + + Session + IP Address + Location + Last Used + + + + + {sessions.length === 0 ? ( + + + No active sessions found + + + ) : ( + sessions.map((session) => ( + + +
+
+ {session.id.includes("mobile") ? ( + + ) : ( + + )} +
+
+ + {session.isCurrentSession ? "Current Session" : "Other Session"} + {session.isCurrentSession && ( + + Active + + )} + + {session.isImpersonation && ( + + Impersonation + + )} + + Signed in {new Date(session.createdAt).toLocaleDateString()} + +
+
+
+ + {session.geoInfo?.ip || '-'} + + + {session.geoInfo?.cityName || 'Unknown'} + + +
+ + {session.lastUsedAt ? fromNow(new Date(session.lastUsedAt)) : "Never"} + + + {session.lastUsedAt ? new Date(session.lastUsedAt).toLocaleDateString() : ""} + +
+
+ + + + + + + handleRevokeSession(session.id)} + disabled={session.isCurrentSession} + className={cn( + "cursor-pointer rounded-lg text-red-500 hover:text-red-600 focus:text-red-500", + session.isCurrentSession ? "opacity-50 cursor-not-allowed" : "" + )} + > + Revoke Session + + + + +
+ )) + )} +
+
+
+ )} +
+
+ ); +}