Refactor security functions

This commit is contained in:
gpt-engineer-app[bot]
2025-10-14 19:38:36 +00:00
parent 1554254c82
commit 95972a0b22
9 changed files with 638 additions and 89 deletions

View File

@@ -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>
</>
);