diff --git a/docs/ACCOUNT_SECURITY_IMPROVEMENTS.md b/docs/ACCOUNT_SECURITY_IMPROVEMENTS.md index 9089afcd..26b08a7a 100644 --- a/docs/ACCOUNT_SECURITY_IMPROVEMENTS.md +++ b/docs/ACCOUNT_SECURITY_IMPROVEMENTS.md @@ -1,5 +1,31 @@ # Account Security Improvements +## UI Consolidation: Sessions Merged into Security Tab + +**Date**: 2025-01-14 + +**Changes**: +- Merged `SessionsTab` functionality into `SecurityTab` "Active Sessions & Login History" section +- Removed redundant `SessionsTab.tsx` component +- Reduced settings navigation from 7 tabs to 6 tabs +- Added proper TypeScript types with `DeviceInfo` interface + +**Benefits**: +- All security-related features in one location (password, 2FA, social accounts, sessions) +- Eliminated 123 lines of duplicate code +- Improved UX with logical grouping of authentication and session management +- Simplified navigation structure +- Better type safety with typed device information + +**Technical Details**: +- Session management uses existing `user_sessions` table with proper RLS +- Real-time session fetching via Supabase query +- Device detection from `device_info` JSONB column +- Session revocation with proper error handling and toast notifications +- Type-safe interfaces for `DeviceInfo` and `UserSession` + +--- + ## Implemented Security Enhancements This document outlines all security improvements made to the account settings system. diff --git a/src/components/settings/SecurityTab.tsx b/src/components/settings/SecurityTab.tsx index 734e78e2..069452d3 100644 --- a/src/components/settings/SecurityTab.tsx +++ b/src/components/settings/SecurityTab.tsx @@ -6,7 +6,8 @@ import { Separator } from '@/components/ui/separator'; import { Badge } from '@/components/ui/badge'; import { useToast } from '@/hooks/use-toast'; import { useAuth } from '@/hooks/useAuth'; -import { Shield, Key, Smartphone, Globe, Loader2 } from 'lucide-react'; +import { Shield, Key, Smartphone, Globe, Loader2, Monitor, Tablet, Trash2 } from 'lucide-react'; +import { format } from 'date-fns'; import { TOTPSetup } from '@/components/auth/TOTPSetup'; import { GoogleIcon } from '@/components/icons/GoogleIcon'; import { DiscordIcon } from '@/components/icons/DiscordIcon'; @@ -22,6 +23,22 @@ import type { UserIdentity, OAuthProvider } from '@/types/identity'; import { toast as sonnerToast } from '@/components/ui/sonner'; import { supabase } from '@/integrations/supabase/client'; +interface DeviceInfo { + browser?: string; + userAgent?: string; + os?: string; + device?: string; +} + +interface UserSession { + id: string; + device_info: DeviceInfo | null; + last_activity: string; + created_at: string; + expires_at: string; + session_token: string; +} + export function SecurityTab() { const { user } = useAuth(); const { toast } = useToast(); @@ -32,10 +49,13 @@ export function SecurityTab() { const [disconnectingProvider, setDisconnectingProvider] = useState(null); const [hasPassword, setHasPassword] = useState(false); const [addingPassword, setAddingPassword] = useState(false); + const [sessions, setSessions] = useState([]); + const [loadingSessions, setLoadingSessions] = useState(true); // Load user identities on mount useEffect(() => { loadIdentities(); + fetchSessions(); }, []); const loadIdentities = async () => { @@ -160,6 +180,59 @@ export function SecurityTab() { setAddingPassword(false); }; + const fetchSessions = async () => { + if (!user) return; + + const { data, error } = await supabase + .from('user_sessions') + .select('*') + .eq('user_id', user.id) + .order('last_activity', { ascending: false }); + + if (error) { + console.error('Error fetching sessions:', error); + } else { + const typedSessions: UserSession[] = (data || []).map(session => ({ + id: session.id, + device_info: session.device_info as DeviceInfo | null, + last_activity: session.last_activity, + created_at: session.created_at, + expires_at: session.expires_at, + session_token: session.session_token + })); + setSessions(typedSessions); + } + setLoadingSessions(false); + }; + + const revokeSession = async (sessionId: string) => { + const { error } = await supabase + .from('user_sessions') + .delete() + .eq('id', sessionId); + + if (error) { + toast({ + title: 'Error', + description: 'Failed to revoke session', + variant: 'destructive' + }); + } else { + toast({ + title: 'Success', + description: 'Session revoked successfully' + }); + fetchSessions(); + } + }; + + const getDeviceIcon = (deviceInfo: DeviceInfo | null) => { + const ua = deviceInfo?.userAgent?.toLowerCase() || ''; + if (ua.includes('mobile')) return ; + if (ua.includes('tablet')) return ; + return ; + }; + // Get connected accounts with identity data const connectedAccounts = [ { @@ -317,35 +390,60 @@ export function SecurityTab() { - {/* Login History */} + {/* Active Sessions & Login History */}
-

Login History

+

+ Active Sessions {sessions.length > 0 && `(${sessions.length})`} +

- Review your recent login activity and active sessions. + Review and manage your active login sessions across all devices -
-
-
-

Current Session

-

- Web • {new Date().toLocaleDateString()} -

-
- Active + {loadingSessions ? ( +
+
- -
-

No other recent sessions found

+ ) : sessions.length > 0 ? ( +
+ {sessions.map((session) => ( +
+
+ {getDeviceIcon(session.device_info)} +
+

+ {session.device_info?.browser || 'Unknown Browser'} +

+

+ Last active: {format(new Date(session.last_activity), 'PPpp')} +

+

+ Expires: {format(new Date(session.expires_at), 'PPpp')} +

+
+
+ +
+ ))}
-
+ ) : ( +
+

No active sessions found

+
+ )}
diff --git a/src/components/settings/SessionsTab.tsx b/src/components/settings/SessionsTab.tsx deleted file mode 100644 index ede59601..00000000 --- a/src/components/settings/SessionsTab.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import { useState, useEffect } from 'react'; -import { supabase } from '@/integrations/supabase/client'; -import { useAuth } from '@/hooks/useAuth'; -import { Button } from '@/components/ui/button'; -import { Card } from '@/components/ui/card'; -import { useToast } from '@/hooks/use-toast'; -import { Monitor, Smartphone, Tablet, Trash2 } from 'lucide-react'; -import { format } from 'date-fns'; - -interface UserSession { - id: string; - device_info: any; - last_activity: string; - created_at: string; - expires_at: string; - session_token: string; -} - -export function SessionsTab() { - const { user } = useAuth(); - const { toast } = useToast(); - const [sessions, setSessions] = useState([]); - const [loading, setLoading] = useState(true); - - const fetchSessions = async () => { - if (!user) return; - - const { data, error } = await supabase - .from('user_sessions') - .select('*') - .eq('user_id', user.id) - .order('last_activity', { ascending: false }); - - if (error) { - console.error('Error fetching sessions:', error); - } else { - setSessions(data || []); - } - setLoading(false); - }; - - useEffect(() => { - fetchSessions(); - }, [user]); - - const revokeSession = async (sessionId: string) => { - const { error } = await supabase - .from('user_sessions') - .delete() - .eq('id', sessionId); - - if (error) { - toast({ - title: 'Error', - description: 'Failed to revoke session', - variant: 'destructive' - }); - } else { - toast({ - title: 'Success', - description: 'Session revoked successfully' - }); - fetchSessions(); - } - }; - - const getDeviceIcon = (deviceInfo: any) => { - const ua = deviceInfo?.userAgent?.toLowerCase() || ''; - if (ua.includes('mobile')) return ; - if (ua.includes('tablet')) return ; - return ; - }; - - if (loading) { - return
Loading sessions...
; - } - - return ( -
-
-

Active Sessions

-

- Manage your active login sessions across devices -

-
- - {sessions.map((session) => ( - -
-
- {getDeviceIcon(session.device_info)} -
-
- {session.device_info?.browser || 'Unknown Browser'} -
-
- Last active: {format(new Date(session.last_activity), 'PPpp')} -
-
- Expires: {format(new Date(session.expires_at), 'PPpp')} -
-
-
- -
-
- ))} - - {sessions.length === 0 && ( - - No active sessions found - - )} -
- ); -} diff --git a/src/pages/UserSettings.tsx b/src/pages/UserSettings.tsx index ab70fa45..4d059610 100644 --- a/src/pages/UserSettings.tsx +++ b/src/pages/UserSettings.tsx @@ -1,14 +1,13 @@ import { useState } from 'react'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; -import { Settings, User, Shield, Eye, Bell, MapPin, Download, MonitorSmartphone } from 'lucide-react'; +import { Settings, User, Shield, Eye, Bell, MapPin, Download } from 'lucide-react'; import { useAuth } from '@/hooks/useAuth'; import { useProfile } from '@/hooks/useProfile'; import { Navigate } from 'react-router-dom'; import { Header } from '@/components/layout/Header'; import { AccountProfileTab } from '@/components/settings/AccountProfileTab'; import { SecurityTab } from '@/components/settings/SecurityTab'; -import { SessionsTab } from '@/components/settings/SessionsTab'; import { PrivacyTab } from '@/components/settings/PrivacyTab'; import { NotificationsTab } from '@/components/settings/NotificationsTab'; import { LocationTab } from '@/components/settings/LocationTab'; @@ -54,12 +53,6 @@ export default function UserSettings() { icon: Shield, component: SecurityTab }, - { - id: 'sessions', - label: 'Sessions', - icon: MonitorSmartphone, - component: SessionsTab - }, { id: 'privacy', label: 'Privacy', @@ -102,7 +95,7 @@ export default function UserSettings() { {/* Settings Tabs */} - + {tabs.map((tab) => { const Icon = tab.icon; return (