mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:51:14 -05:00
Refactor: Consolidate Sessions into Security Tab
This commit is contained in:
@@ -1,5 +1,31 @@
|
|||||||
# Account Security Improvements
|
# 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
|
## Implemented Security Enhancements
|
||||||
|
|
||||||
This document outlines all security improvements made to the account settings system.
|
This document outlines all security improvements made to the account settings system.
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
import { useToast } from '@/hooks/use-toast';
|
||||||
import { useAuth } from '@/hooks/useAuth';
|
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 { TOTPSetup } from '@/components/auth/TOTPSetup';
|
||||||
import { GoogleIcon } from '@/components/icons/GoogleIcon';
|
import { GoogleIcon } from '@/components/icons/GoogleIcon';
|
||||||
import { DiscordIcon } from '@/components/icons/DiscordIcon';
|
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 { toast as sonnerToast } from '@/components/ui/sonner';
|
||||||
import { supabase } from '@/integrations/supabase/client';
|
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() {
|
export function SecurityTab() {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
@@ -32,10 +49,13 @@ export function SecurityTab() {
|
|||||||
const [disconnectingProvider, setDisconnectingProvider] = useState<OAuthProvider | null>(null);
|
const [disconnectingProvider, setDisconnectingProvider] = useState<OAuthProvider | null>(null);
|
||||||
const [hasPassword, setHasPassword] = useState(false);
|
const [hasPassword, setHasPassword] = useState(false);
|
||||||
const [addingPassword, setAddingPassword] = useState(false);
|
const [addingPassword, setAddingPassword] = useState(false);
|
||||||
|
const [sessions, setSessions] = useState<UserSession[]>([]);
|
||||||
|
const [loadingSessions, setLoadingSessions] = useState(true);
|
||||||
|
|
||||||
// Load user identities on mount
|
// Load user identities on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadIdentities();
|
loadIdentities();
|
||||||
|
fetchSessions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const loadIdentities = async () => {
|
const loadIdentities = async () => {
|
||||||
@@ -160,6 +180,59 @@ export function SecurityTab() {
|
|||||||
setAddingPassword(false);
|
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 <Smartphone className="w-4 h-4" />;
|
||||||
|
if (ua.includes('tablet')) return <Tablet className="w-4 h-4" />;
|
||||||
|
return <Monitor className="w-4 h-4" />;
|
||||||
|
};
|
||||||
|
|
||||||
// Get connected accounts with identity data
|
// Get connected accounts with identity data
|
||||||
const connectedAccounts = [
|
const connectedAccounts = [
|
||||||
{
|
{
|
||||||
@@ -317,35 +390,60 @@ export function SecurityTab() {
|
|||||||
|
|
||||||
<Separator />
|
<Separator />
|
||||||
|
|
||||||
{/* Login History */}
|
{/* Active Sessions & Login History */}
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Shield className="w-5 h-5" />
|
<Shield className="w-5 h-5" />
|
||||||
<h3 className="text-lg font-medium">Login History</h3>
|
<h3 className="text-lg font-medium">
|
||||||
|
Active Sessions {sessions.length > 0 && `(${sessions.length})`}
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
Review your recent login activity and active sessions.
|
Review and manage your active login sessions across all devices
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
{loadingSessions ? (
|
||||||
|
<div className="flex items-center justify-center p-8">
|
||||||
|
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : sessions.length > 0 ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="flex items-center justify-between p-3 border rounded-lg">
|
{sessions.map((session) => (
|
||||||
|
<div key={session.id} className="flex items-start justify-between p-3 border rounded-lg">
|
||||||
|
<div className="flex gap-3">
|
||||||
|
{getDeviceIcon(session.device_info)}
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium">Current Session</p>
|
<p className="font-medium">
|
||||||
|
{session.device_info?.browser || 'Unknown Browser'}
|
||||||
|
</p>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Web • {new Date().toLocaleDateString()}
|
Last active: {format(new Date(session.last_activity), 'PPpp')}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Expires: {format(new Date(session.expires_at), 'PPpp')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Badge variant="secondary">Active</Badge>
|
|
||||||
</div>
|
</div>
|
||||||
|
<Button
|
||||||
<div className="text-center p-4 text-muted-foreground">
|
variant="destructive"
|
||||||
<p className="text-sm">No other recent sessions found</p>
|
size="sm"
|
||||||
|
onClick={() => revokeSession(session.id)}
|
||||||
|
>
|
||||||
|
<Trash2 className="w-4 h-4 mr-2" />
|
||||||
|
Revoke
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center p-8 text-muted-foreground">
|
||||||
|
<p className="text-sm">No active sessions found</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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<UserSession[]>([]);
|
|
||||||
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 <Smartphone className="w-4 h-4" />;
|
|
||||||
if (ua.includes('tablet')) return <Tablet className="w-4 h-4" />;
|
|
||||||
return <Monitor className="w-4 h-4" />;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <div className="text-muted-foreground">Loading sessions...</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-lg font-semibold">Active Sessions</h3>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Manage your active login sessions across devices
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{sessions.map((session) => (
|
|
||||||
<Card key={session.id} className="p-4">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex gap-3">
|
|
||||||
{getDeviceIcon(session.device_info)}
|
|
||||||
<div>
|
|
||||||
<div className="font-medium">
|
|
||||||
{session.device_info?.browser || 'Unknown Browser'}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Last active: {format(new Date(session.last_activity), 'PPpp')}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm text-muted-foreground">
|
|
||||||
Expires: {format(new Date(session.expires_at), 'PPpp')}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => revokeSession(session.id)}
|
|
||||||
>
|
|
||||||
<Trash2 className="w-4 h-4 mr-2" />
|
|
||||||
Revoke
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{sessions.length === 0 && (
|
|
||||||
<Card className="p-8 text-center text-muted-foreground">
|
|
||||||
No active sessions found
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
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 { useAuth } from '@/hooks/useAuth';
|
||||||
import { useProfile } from '@/hooks/useProfile';
|
import { useProfile } from '@/hooks/useProfile';
|
||||||
import { Navigate } from 'react-router-dom';
|
import { Navigate } from 'react-router-dom';
|
||||||
import { Header } from '@/components/layout/Header';
|
import { Header } from '@/components/layout/Header';
|
||||||
import { AccountProfileTab } from '@/components/settings/AccountProfileTab';
|
import { AccountProfileTab } from '@/components/settings/AccountProfileTab';
|
||||||
import { SecurityTab } from '@/components/settings/SecurityTab';
|
import { SecurityTab } from '@/components/settings/SecurityTab';
|
||||||
import { SessionsTab } from '@/components/settings/SessionsTab';
|
|
||||||
import { PrivacyTab } from '@/components/settings/PrivacyTab';
|
import { PrivacyTab } from '@/components/settings/PrivacyTab';
|
||||||
import { NotificationsTab } from '@/components/settings/NotificationsTab';
|
import { NotificationsTab } from '@/components/settings/NotificationsTab';
|
||||||
import { LocationTab } from '@/components/settings/LocationTab';
|
import { LocationTab } from '@/components/settings/LocationTab';
|
||||||
@@ -54,12 +53,6 @@ export default function UserSettings() {
|
|||||||
icon: Shield,
|
icon: Shield,
|
||||||
component: SecurityTab
|
component: SecurityTab
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'sessions',
|
|
||||||
label: 'Sessions',
|
|
||||||
icon: MonitorSmartphone,
|
|
||||||
component: SessionsTab
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'privacy',
|
id: 'privacy',
|
||||||
label: 'Privacy',
|
label: 'Privacy',
|
||||||
@@ -102,7 +95,7 @@ export default function UserSettings() {
|
|||||||
|
|
||||||
{/* Settings Tabs */}
|
{/* Settings Tabs */}
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
|
||||||
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-7 h-auto p-1">
|
<TabsList className="grid w-full grid-cols-2 lg:grid-cols-6 h-auto p-1">
|
||||||
{tabs.map((tab) => {
|
{tabs.map((tab) => {
|
||||||
const Icon = tab.icon;
|
const Icon = tab.icon;
|
||||||
return (
|
return (
|
||||||
|
|||||||
Reference in New Issue
Block a user