mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-22 04:51:13 -05:00
Refactor security functions
This commit is contained in:
@@ -8,6 +8,7 @@ import { handleError, handleSuccess } from '@/lib/errorHandler';
|
||||
import { useAuth } from '@/hooks/useAuth';
|
||||
import { Shield, Key, Smartphone, Globe, Loader2, Monitor, Tablet, Trash2 } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { TOTPSetup } from '@/components/auth/TOTPSetup';
|
||||
import { GoogleIcon } from '@/components/icons/GoogleIcon';
|
||||
import { DiscordIcon } from '@/components/icons/DiscordIcon';
|
||||
@@ -20,18 +21,10 @@ import {
|
||||
addPasswordToAccount
|
||||
} from '@/lib/identityService';
|
||||
import type { UserIdentity, OAuthProvider } from '@/types/identity';
|
||||
import type { AuthSession } from '@/types/auth';
|
||||
import { supabase } from '@/integrations/supabase/client';
|
||||
|
||||
interface AuthSession {
|
||||
id: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
refreshed_at: string | null;
|
||||
user_agent: string | null;
|
||||
ip: unknown;
|
||||
not_after: string | null;
|
||||
aal: 'aal1' | 'aal2' | 'aal3' | null;
|
||||
}
|
||||
import { logger } from '@/lib/logger';
|
||||
import { SessionRevokeConfirmDialog } from './SessionRevokeConfirmDialog';
|
||||
|
||||
export function SecurityTab() {
|
||||
const { user } = useAuth();
|
||||
@@ -44,6 +37,7 @@ export function SecurityTab() {
|
||||
const [addingPassword, setAddingPassword] = useState(false);
|
||||
const [sessions, setSessions] = useState<AuthSession[]>([]);
|
||||
const [loadingSessions, setLoadingSessions] = useState(true);
|
||||
const [sessionToRevoke, setSessionToRevoke] = useState<{ id: string; isCurrent: boolean } | null>(null);
|
||||
|
||||
// Load user identities on mount
|
||||
useEffect(() => {
|
||||
@@ -61,8 +55,12 @@ export function SecurityTab() {
|
||||
const hasEmailProvider = fetchedIdentities.some(i => i.provider === 'email');
|
||||
setHasPassword(hasEmailProvider);
|
||||
} catch (error) {
|
||||
console.error('Failed to load identities:', error);
|
||||
handleError(error, { action: 'Load connected accounts' });
|
||||
logger.error('Failed to load identities', {
|
||||
userId: user?.id,
|
||||
action: 'load_identities',
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
handleError(error, { action: 'Load connected accounts', userId: user?.id });
|
||||
} finally {
|
||||
setLoadingIdentities(false);
|
||||
}
|
||||
@@ -151,23 +149,55 @@ export function SecurityTab() {
|
||||
const { data, error } = await supabase.rpc('get_my_sessions');
|
||||
|
||||
if (error) {
|
||||
console.error('Error fetching sessions:', error);
|
||||
handleError(error, { action: 'Load sessions' });
|
||||
logger.error('Failed to fetch sessions', {
|
||||
userId: user.id,
|
||||
action: 'fetch_sessions',
|
||||
error: error.message
|
||||
});
|
||||
handleError(error, { action: 'Load sessions', userId: user.id });
|
||||
} else {
|
||||
setSessions(data || []);
|
||||
setSessions((data as AuthSession[]) || []);
|
||||
}
|
||||
setLoadingSessions(false);
|
||||
};
|
||||
|
||||
const revokeSession = async (sessionId: string) => {
|
||||
const { error } = await supabase.rpc('revoke_my_session', { session_id: sessionId });
|
||||
const initiateSessionRevoke = async (sessionId: string) => {
|
||||
// Get current session to check if revoking self
|
||||
const { data: { session: currentSession } } = await supabase.auth.getSession();
|
||||
const isCurrentSession = currentSession && sessions.some(s =>
|
||||
s.id === sessionId && s.refreshed_at === currentSession.access_token
|
||||
);
|
||||
|
||||
setSessionToRevoke({ id: sessionId, isCurrent: !!isCurrentSession });
|
||||
};
|
||||
|
||||
const confirmRevokeSession = async () => {
|
||||
if (!sessionToRevoke) return;
|
||||
|
||||
const { error } = await supabase.rpc('revoke_my_session', { session_id: sessionToRevoke.id });
|
||||
|
||||
if (error) {
|
||||
handleError(error, { action: 'Revoke session' });
|
||||
logger.error('Failed to revoke session', {
|
||||
userId: user?.id,
|
||||
action: 'revoke_session',
|
||||
sessionId: sessionToRevoke.id,
|
||||
error: error.message
|
||||
});
|
||||
handleError(error, { action: 'Revoke session', userId: user?.id });
|
||||
} else {
|
||||
handleSuccess('Success', 'Session revoked successfully');
|
||||
fetchSessions();
|
||||
|
||||
if (sessionToRevoke.isCurrent) {
|
||||
// Redirect to login after revoking current session
|
||||
setTimeout(() => {
|
||||
window.location.href = '/auth';
|
||||
}, 1000);
|
||||
} else {
|
||||
fetchSessions();
|
||||
}
|
||||
}
|
||||
|
||||
setSessionToRevoke(null);
|
||||
};
|
||||
|
||||
const getDeviceIcon = (userAgent: string | null) => {
|
||||
@@ -365,8 +395,19 @@ export function SecurityTab() {
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{loadingSessions ? (
|
||||
<div className="flex items-center justify-center p-8">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-muted-foreground" />
|
||||
<div className="space-y-3">
|
||||
{[1, 2, 3].map(i => (
|
||||
<div key={i} className="flex items-start justify-between p-3 border rounded-lg">
|
||||
<div className="flex gap-3 flex-1">
|
||||
<Skeleton className="w-4 h-4 rounded" />
|
||||
<div className="flex-1 space-y-2">
|
||||
<Skeleton className="h-4 w-32" />
|
||||
<Skeleton className="h-3 w-48" />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className="w-20 h-8" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : sessions.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
@@ -379,7 +420,11 @@ export function SecurityTab() {
|
||||
{getBrowserName(session.user_agent)}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{session.ip && `${session.ip} • `}
|
||||
{session.ip && (
|
||||
<span title="Last 8 characters of hashed IP address for privacy">
|
||||
{session.ip} •{' '}
|
||||
</span>
|
||||
)}
|
||||
Last active: {format(new Date(session.refreshed_at || session.created_at), 'PPpp')}
|
||||
{session.aal === 'aal2' && ' • MFA'}
|
||||
</p>
|
||||
@@ -393,7 +438,7 @@ export function SecurityTab() {
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => revokeSession(session.id)}
|
||||
onClick={() => initiateSessionRevoke(session.id)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 mr-2" />
|
||||
Revoke
|
||||
@@ -409,6 +454,13 @@ export function SecurityTab() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<SessionRevokeConfirmDialog
|
||||
open={!!sessionToRevoke}
|
||||
onOpenChange={(open) => !open && setSessionToRevoke(null)}
|
||||
onConfirm={confirmRevokeSession}
|
||||
isCurrentSession={sessionToRevoke?.isCurrent ?? false}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user