veza/apps/web/src/features/auth/pages/SessionsPage.tsx

268 lines
7.8 KiB
TypeScript
Raw Normal View History

import { useState, useEffect } from 'react';
import { apiClient } from '@/services/api/client';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
import { LoadingSpinner } from '@/components/ui/loading-spinner';
2025-12-13 02:34:34 +00:00
import {
Shield,
Smartphone,
Monitor,
Globe,
Trash2,
LogOut,
} from 'lucide-react';
interface Session {
id: string;
ip_address: string;
user_agent: string;
last_activity: string;
created_at: string;
is_current: boolean;
}
interface SessionsResponse {
sessions: Session[];
count: number;
}
export function SessionsPage() {
const [sessions, setSessions] = useState<Session[]>([]);
const [loading, setLoading] = useState(true);
const [revoking, setRevoking] = useState<string | null>(null);
const [revokingAll, setRevokingAll] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetchSessions();
}, []);
const fetchSessions = async () => {
try {
setLoading(true);
setError(null);
const response = await apiClient.get<SessionsResponse>('/auth/sessions');
setSessions(response.data.sessions);
} catch (error: any) {
console.error('Failed to fetch sessions', error);
setError(
2025-12-13 02:34:34 +00:00
error.response?.data?.error ||
'Failed to load sessions. Please try again.',
);
} finally {
setLoading(false);
}
};
const revokeSession = async (sessionId: string) => {
try {
setRevoking(sessionId);
setError(null);
await apiClient.delete(`/auth/sessions/${sessionId}`);
await fetchSessions();
} catch (error: any) {
console.error('Failed to revoke session', error);
setError(
2025-12-13 02:34:34 +00:00
error.response?.data?.error ||
'Failed to revoke session. Please try again.',
);
} finally {
setRevoking(null);
}
};
const revokeAllOther = async () => {
if (
!window.confirm(
2025-12-13 02:34:34 +00:00
'Are you sure you want to revoke all other sessions? You will remain logged in on this device.',
)
) {
return;
}
try {
setRevokingAll(true);
setError(null);
await apiClient.delete('/auth/sessions');
await fetchSessions();
} catch (error: any) {
console.error('Failed to revoke sessions', error);
setError(
2025-12-13 02:34:34 +00:00
error.response?.data?.error ||
'Failed to revoke sessions. Please try again.',
);
} finally {
setRevokingAll(false);
}
};
const getDeviceIcon = (userAgent: string) => {
const ua = userAgent.toLowerCase();
2025-12-13 02:34:34 +00:00
if (
ua.includes('mobile') ||
ua.includes('android') ||
ua.includes('iphone')
) {
return <Smartphone className="h-5 w-5" />;
}
if (ua.includes('tablet') || ua.includes('ipad')) {
return <Monitor className="h-5 w-5" />;
}
return <Monitor className="h-5 w-5" />;
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return new Intl.DateTimeFormat('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
}).format(date);
};
if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<LoadingSpinner />
</div>
);
}
return (
<div className="space-y-6">
{/* En-tête */}
<div>
<h1 className="text-3xl font-bold tracking-tight">Active Sessions</h1>
<p className="text-muted-foreground">
Manage your active sessions and sign out from other devices
</p>
</div>
{/* Message d'erreur */}
{error && (
<div className="bg-destructive/10 text-destructive px-4 py-3 rounded-md">
{error}
</div>
)}
{/* Bouton pour révoquer toutes les autres sessions */}
<div className="flex justify-end">
<Button
onClick={revokeAllOther}
disabled={revokingAll || sessions.length <= 1}
variant="destructive"
>
{revokingAll ? (
<>
<LoadingSpinner className="mr-2 h-4 w-4" />
Revoking...
</>
) : (
<>
<LogOut className="mr-2 h-4 w-4" />
Revoke All Other Sessions
</>
)}
</Button>
</div>
{/* Liste des sessions */}
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Shield className="h-5 w-5" />
Sessions ({sessions.length})
</CardTitle>
<CardDescription>
These are the devices where you're currently signed in
</CardDescription>
</CardHeader>
<CardContent>
{sessions.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No active sessions found.
</div>
) : (
<div className="space-y-4">
2025-12-13 02:34:34 +00:00
{sessions.map((session) => (
<div
key={session.id}
className={`flex items-center justify-between p-4 border rounded-lg ${
session.is_current
? 'border-primary bg-primary/5'
: 'border-border'
}`}
>
<div className="flex items-start gap-4 flex-1">
<div className="mt-1">
{getDeviceIcon(session.user_agent)}
</div>
<div className="flex-1 space-y-1">
<div className="flex items-center gap-2">
<p className="font-medium">
{session.user_agent || 'Unknown Device'}
</p>
{session.is_current && (
<span className="px-2 py-0.5 text-xs font-medium bg-primary text-primary-foreground rounded">
Current Session
</span>
)}
</div>
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
<div className="flex items-center gap-1">
<Globe className="h-4 w-4" />
<span>{session.ip_address || 'Unknown IP'}</span>
</div>
<div>
<span className="font-medium">Created:</span>{' '}
{formatDate(session.created_at)}
</div>
{session.last_activity && (
<div>
<span className="font-medium">Last activity:</span>{' '}
{formatDate(session.last_activity)}
</div>
)}
</div>
</div>
</div>
{!session.is_current && (
<Button
onClick={() => revokeSession(session.id)}
disabled={revoking === session.id}
variant="destructive"
size="sm"
>
{revoking === session.id ? (
<>
<LoadingSpinner className="mr-2 h-4 w-4" />
Revoking...
</>
) : (
<>
<Trash2 className="mr-2 h-4 w-4" />
Revoke
</>
)}
</Button>
)}
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
);
}
export default SessionsPage;