From 4c09e0dbd138696a836df08d2596b546354ec09e Mon Sep 17 00:00:00 2001 From: robertocarlous Date: Sat, 17 Oct 2026 20:21:43 +0100 Subject: [PATCH] feat: add session security dashboard in account settings (#1111) Add session management surface with active sessions list, device/browser/IP and last active time, current session badge and non-revokable state, single revoke with confirmation, and revoke-all-other sessions action. Add sessions API service, React Query hooks, settings page and route. --- .../settings/SessionSecuritySection.tsx | 310 ++++++++++++++++++ src/hooks/useSessions.ts | 77 +++++ src/lib/queryKeys.ts | 4 + src/pages/AccountSettingsPage.tsx | 47 +++ src/routes.tsx | 9 + src/services/session.service.ts | 70 ++++ 6 files changed, 517 insertions(+) create mode 100644 src/components/settings/SessionSecuritySection.tsx create mode 100644 src/hooks/useSessions.ts create mode 100644 src/pages/AccountSettingsPage.tsx create mode 100644 src/services/session.service.ts diff --git a/src/components/settings/SessionSecuritySection.tsx b/src/components/settings/SessionSecuritySection.tsx new file mode 100644 index 00000000..bc9cf8b7 --- /dev/null +++ b/src/components/settings/SessionSecuritySection.tsx @@ -0,0 +1,310 @@ +import { useState } from 'react'; +import { + Laptop, + Smartphone, + Monitor, + ShieldCheck, + Trash2, + LogOut, +} from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { AsyncButton } from '@/components/ui/async-button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { cn } from '@/lib/utils'; +import type { Session } from '@/services/session.service'; +import { formatTimestampTooltip } from '@/utils/time.utils'; + +export interface SessionSecuritySectionProps { + sessions: Session[]; + isLoading?: boolean; + isError?: boolean; + isRevoking?: boolean; + onRevokeSession: (sessionId: string) => void; + onRevokeAllOtherSessions: () => void; + className?: string; +} + +function DeviceIcon({ device }: { device: string }) { + const label = device.toLowerCase(); + const className = 'size-5 text-white/60'; + if (label.includes('iphone') || label.includes('android') || label.includes('phone')) { + return