mirror of
https://github.com/pezkuwichain/pwap.git
synced 2026-04-24 17:47:55 +00:00
Reorganize repository into monorepo structure
Restructured the project to support multiple frontend applications: - Move web app to web/ directory - Create pezkuwi-sdk-ui/ for Polkadot SDK clone (planned) - Create mobile/ directory for mobile app development - Add shared/ directory with common utilities, types, and blockchain code - Update README.md with comprehensive documentation - Remove obsolete DKSweb/ directory This monorepo structure enables better code sharing and organized development across web, mobile, and SDK UI projects.
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Shield, Save, RefreshCw, Lock, Unlock } from 'lucide-react';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
interface Role {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
permissions: Record<string, boolean>;
|
||||
is_system: boolean;
|
||||
}
|
||||
|
||||
const PERMISSION_CATEGORIES = {
|
||||
governance: {
|
||||
title: 'Governance',
|
||||
permissions: {
|
||||
create_proposal: 'Create Proposals',
|
||||
vote_proposal: 'Vote on Proposals',
|
||||
delegate_vote: 'Delegate Voting Power',
|
||||
manage_treasury: 'Manage Treasury',
|
||||
}
|
||||
},
|
||||
moderation: {
|
||||
title: 'Moderation',
|
||||
permissions: {
|
||||
moderate_content: 'Moderate Content',
|
||||
ban_users: 'Ban Users',
|
||||
delete_posts: 'Delete Posts',
|
||||
pin_posts: 'Pin Posts',
|
||||
}
|
||||
},
|
||||
administration: {
|
||||
title: 'Administration',
|
||||
permissions: {
|
||||
manage_users: 'Manage Users',
|
||||
manage_roles: 'Manage Roles',
|
||||
view_analytics: 'View Analytics',
|
||||
system_settings: 'System Settings',
|
||||
}
|
||||
},
|
||||
security: {
|
||||
title: 'Security',
|
||||
permissions: {
|
||||
view_audit_logs: 'View Audit Logs',
|
||||
manage_sessions: 'Manage Sessions',
|
||||
configure_2fa: 'Configure 2FA',
|
||||
access_api: 'Access API',
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function PermissionEditor() {
|
||||
const [roles, setRoles] = useState<Role[]>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<Role | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
loadRoles();
|
||||
}, []);
|
||||
|
||||
const loadRoles = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('roles')
|
||||
.select('*')
|
||||
.order('name');
|
||||
|
||||
if (error) throw error;
|
||||
setRoles(data || []);
|
||||
if (data && data.length > 0) {
|
||||
setSelectedRole(data[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading roles:', error);
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to load roles',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePermission = (category: string, permission: string) => {
|
||||
if (!selectedRole || selectedRole.is_system) return;
|
||||
|
||||
const fullPermission = `${category}.${permission}`;
|
||||
setSelectedRole({
|
||||
...selectedRole,
|
||||
permissions: {
|
||||
...selectedRole.permissions,
|
||||
[fullPermission]: !selectedRole.permissions[fullPermission]
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const savePermissions = async () => {
|
||||
if (!selectedRole) return;
|
||||
|
||||
setSaving(true);
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('roles')
|
||||
.update({ permissions: selectedRole.permissions })
|
||||
.eq('id', selectedRole.id);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Success',
|
||||
description: 'Permissions updated successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to save permissions',
|
||||
variant: 'destructive',
|
||||
});
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetPermissions = () => {
|
||||
if (!selectedRole) return;
|
||||
const original = roles.find(r => r.id === selectedRole.id);
|
||||
if (original) {
|
||||
setSelectedRole(original);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Permission Editor
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={selectedRole?.id} onValueChange={(id) => {
|
||||
const role = roles.find(r => r.id === id);
|
||||
if (role) setSelectedRole(role);
|
||||
}}>
|
||||
<TabsList className="grid grid-cols-4 w-full">
|
||||
{roles.map(role => (
|
||||
<TabsTrigger key={role.id} value={role.id}>
|
||||
{role.name}
|
||||
{role.is_system && (
|
||||
<Lock className="h-3 w-3 ml-1" />
|
||||
)}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{selectedRole && (
|
||||
<TabsContent value={selectedRole.id} className="space-y-6 mt-6">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{selectedRole.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">{selectedRole.description}</p>
|
||||
{selectedRole.is_system && (
|
||||
<Badge variant="secondary" className="mt-2">
|
||||
<Lock className="h-3 w-3 mr-1" />
|
||||
System Role (Read Only)
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{!selectedRole.is_system && (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={resetPermissions}
|
||||
>
|
||||
<RefreshCw className="h-4 w-4 mr-1" />
|
||||
Reset
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={savePermissions}
|
||||
disabled={saving}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
{Object.entries(PERMISSION_CATEGORIES).map(([categoryKey, category]) => (
|
||||
<div key={categoryKey} className="space-y-3">
|
||||
<h4 className="font-medium text-sm">{category.title}</h4>
|
||||
<div className="space-y-2">
|
||||
{Object.entries(category.permissions).map(([permKey, permName]) => {
|
||||
const fullPerm = `${categoryKey}.${permKey}`;
|
||||
const isEnabled = selectedRole.permissions[fullPerm] || false;
|
||||
|
||||
return (
|
||||
<div key={permKey} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-2">
|
||||
{isEnabled ? (
|
||||
<Unlock className="h-4 w-4 text-green-500" />
|
||||
) : (
|
||||
<Lock className="h-4 w-4 text-muted-foreground" />
|
||||
)}
|
||||
<span className="text-sm">{permName}</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={isEnabled}
|
||||
disabled={selectedRole.is_system}
|
||||
onCheckedChange={() => togglePermission(categoryKey, permKey)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</TabsContent>
|
||||
)}
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,291 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { Shield, AlertTriangle, CheckCircle, XCircle, TrendingUp, Users, Key, Activity } from 'lucide-react';
|
||||
import { LineChart, Line, AreaChart, Area, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
|
||||
|
||||
interface SecurityMetrics {
|
||||
totalUsers: number;
|
||||
activeUsers: number;
|
||||
twoFactorEnabled: number;
|
||||
suspiciousActivities: number;
|
||||
failedLogins: number;
|
||||
securityScore: number;
|
||||
}
|
||||
|
||||
interface AuditLog {
|
||||
id: string;
|
||||
action: string;
|
||||
user_id: string;
|
||||
ip_address: string;
|
||||
created_at: string;
|
||||
severity: 'low' | 'medium' | 'high' | 'critical';
|
||||
}
|
||||
|
||||
export function SecurityAudit() {
|
||||
const [metrics, setMetrics] = useState<SecurityMetrics>({
|
||||
totalUsers: 0,
|
||||
activeUsers: 0,
|
||||
twoFactorEnabled: 0,
|
||||
suspiciousActivities: 0,
|
||||
failedLogins: 0,
|
||||
securityScore: 0,
|
||||
});
|
||||
const [auditLogs, setAuditLogs] = useState<AuditLog[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
loadSecurityData();
|
||||
}, []);
|
||||
|
||||
const loadSecurityData = async () => {
|
||||
try {
|
||||
// Load user metrics
|
||||
const { data: users } = await supabase
|
||||
.from('profiles')
|
||||
.select('id, created_at');
|
||||
|
||||
const { data: twoFactor } = await supabase
|
||||
.from('two_factor_auth')
|
||||
.select('user_id')
|
||||
.eq('enabled', true);
|
||||
|
||||
const { data: sessions } = await supabase
|
||||
.from('user_sessions')
|
||||
.select('user_id')
|
||||
.eq('is_active', true);
|
||||
|
||||
const { data: logs } = await supabase
|
||||
.from('activity_logs')
|
||||
.select('*')
|
||||
.order('created_at', { ascending: false })
|
||||
.limit(100);
|
||||
|
||||
// Calculate metrics
|
||||
const totalUsers = users?.length || 0;
|
||||
const activeUsers = sessions?.length || 0;
|
||||
const twoFactorEnabled = twoFactor?.length || 0;
|
||||
const suspiciousActivities = logs?.filter(l =>
|
||||
l.action.includes('failed') || l.action.includes('suspicious')
|
||||
).length || 0;
|
||||
const failedLogins = logs?.filter(l =>
|
||||
l.action === 'login_failed'
|
||||
).length || 0;
|
||||
|
||||
// Calculate security score
|
||||
const score = Math.round(
|
||||
((twoFactorEnabled / Math.max(totalUsers, 1)) * 40) +
|
||||
((activeUsers / Math.max(totalUsers, 1)) * 20) +
|
||||
(Math.max(0, 40 - (suspiciousActivities * 2)))
|
||||
);
|
||||
|
||||
setMetrics({
|
||||
totalUsers,
|
||||
activeUsers,
|
||||
twoFactorEnabled,
|
||||
suspiciousActivities,
|
||||
failedLogins,
|
||||
securityScore: score,
|
||||
});
|
||||
|
||||
setAuditLogs(logs || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading security data:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const getScoreColor = (score: number) => {
|
||||
if (score >= 80) return 'text-green-500';
|
||||
if (score >= 60) return 'text-yellow-500';
|
||||
if (score >= 40) return 'text-orange-500';
|
||||
return 'text-red-500';
|
||||
};
|
||||
|
||||
const getScoreBadge = (score: number) => {
|
||||
if (score >= 80) return { text: 'Excellent', variant: 'default' as const };
|
||||
if (score >= 60) return { text: 'Good', variant: 'secondary' as const };
|
||||
if (score >= 40) return { text: 'Fair', variant: 'outline' as const };
|
||||
return { text: 'Poor', variant: 'destructive' as const };
|
||||
};
|
||||
|
||||
const pieData = [
|
||||
{ name: '2FA Enabled', value: metrics.twoFactorEnabled, color: '#10b981' },
|
||||
{ name: 'No 2FA', value: metrics.totalUsers - metrics.twoFactorEnabled, color: '#ef4444' },
|
||||
];
|
||||
|
||||
const activityData = [
|
||||
{ name: 'Mon', logins: 45, failures: 2 },
|
||||
{ name: 'Tue', logins: 52, failures: 3 },
|
||||
{ name: 'Wed', logins: 48, failures: 1 },
|
||||
{ name: 'Thu', logins: 61, failures: 4 },
|
||||
{ name: 'Fri', logins: 55, failures: 2 },
|
||||
{ name: 'Sat', logins: 32, failures: 1 },
|
||||
{ name: 'Sun', logins: 28, failures: 0 },
|
||||
];
|
||||
|
||||
const scoreBadge = getScoreBadge(metrics.securityScore);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Security Score Card */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<Shield className="h-5 w-5" />
|
||||
Security Score
|
||||
</span>
|
||||
<Badge variant={scoreBadge.variant}>{scoreBadge.text}</Badge>
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
<div className="text-center">
|
||||
<div className={`text-6xl font-bold ${getScoreColor(metrics.securityScore)}`}>
|
||||
{metrics.securityScore}
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-2">Out of 100</p>
|
||||
</div>
|
||||
<Progress value={metrics.securityScore} className="h-3" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Metrics Grid */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Total Users</p>
|
||||
<p className="text-2xl font-bold">{metrics.totalUsers}</p>
|
||||
</div>
|
||||
<Users className="h-8 w-8 text-blue-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">2FA Enabled</p>
|
||||
<p className="text-2xl font-bold">{metrics.twoFactorEnabled}</p>
|
||||
</div>
|
||||
<Key className="h-8 w-8 text-green-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Active Sessions</p>
|
||||
<p className="text-2xl font-bold">{metrics.activeUsers}</p>
|
||||
</div>
|
||||
<Activity className="h-8 w-8 text-purple-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">Suspicious</p>
|
||||
<p className="text-2xl font-bold">{metrics.suspiciousActivities}</p>
|
||||
</div>
|
||||
<AlertTriangle className="h-8 w-8 text-orange-500" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Charts */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Login Activity</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={activityData}>
|
||||
<CartesianGrid strokeDasharray="3 3" />
|
||||
<XAxis dataKey="name" />
|
||||
<YAxis />
|
||||
<Tooltip />
|
||||
<Area type="monotone" dataKey="logins" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.6} />
|
||||
<Area type="monotone" dataKey="failures" stroke="#ef4444" fill="#ef4444" fillOpacity={0.6} />
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>2FA Adoption</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={pieData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
labelLine={false}
|
||||
label={(entry) => `${entry.name}: ${entry.value}`}
|
||||
outerRadius={80}
|
||||
fill="#8884d8"
|
||||
dataKey="value"
|
||||
>
|
||||
{pieData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip />
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Recent Security Events */}
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Security Events</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{auditLogs.slice(0, 10).map((log) => (
|
||||
<div key={log.id} className="flex items-center justify-between p-3 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
{log.severity === 'critical' && <XCircle className="h-5 w-5 text-red-500" />}
|
||||
{log.severity === 'high' && <AlertTriangle className="h-5 w-5 text-orange-500" />}
|
||||
{log.severity === 'medium' && <AlertTriangle className="h-5 w-5 text-yellow-500" />}
|
||||
{log.severity === 'low' && <CheckCircle className="h-5 w-5 text-green-500" />}
|
||||
<div>
|
||||
<p className="font-medium">{log.action}</p>
|
||||
<p className="text-sm text-muted-foreground">IP: {log.ip_address}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant={
|
||||
log.severity === 'critical' ? 'destructive' :
|
||||
log.severity === 'high' ? 'destructive' :
|
||||
log.severity === 'medium' ? 'secondary' :
|
||||
'outline'
|
||||
}>
|
||||
{log.severity}
|
||||
</Badge>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { supabase } from '@/lib/supabase';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Monitor, Shield, LogOut, AlertTriangle, Activity } from 'lucide-react';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
interface Session {
|
||||
id: string;
|
||||
user_id: string;
|
||||
ip_address: string;
|
||||
user_agent: string;
|
||||
created_at: string;
|
||||
last_activity: string;
|
||||
is_active: boolean;
|
||||
profiles: {
|
||||
username: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
export function SessionMonitor() {
|
||||
const [sessions, setSessions] = useState<Session[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
const interval = setInterval(loadSessions, 30000);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
const loadSessions = async () => {
|
||||
try {
|
||||
const { data, error } = await supabase
|
||||
.from('user_sessions')
|
||||
.select(`
|
||||
*,
|
||||
profiles:user_id (username, email)
|
||||
`)
|
||||
.order('last_activity', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
setSessions(data || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading sessions:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const terminateSession = async (sessionId: string) => {
|
||||
try {
|
||||
const { error } = await supabase
|
||||
.from('user_sessions')
|
||||
.update({ is_active: false })
|
||||
.eq('id', sessionId);
|
||||
|
||||
if (error) throw error;
|
||||
|
||||
toast({
|
||||
title: 'Session Terminated',
|
||||
description: 'The session has been successfully terminated.',
|
||||
});
|
||||
loadSessions();
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: 'Error',
|
||||
description: 'Failed to terminate session',
|
||||
variant: 'destructive',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const getDeviceInfo = (userAgent: string) => {
|
||||
if (userAgent.includes('Mobile')) return 'Mobile';
|
||||
if (userAgent.includes('Tablet')) return 'Tablet';
|
||||
return 'Desktop';
|
||||
};
|
||||
|
||||
const getActivityStatus = (lastActivity: string) => {
|
||||
const diff = Date.now() - new Date(lastActivity).getTime();
|
||||
const minutes = Math.floor(diff / 60000);
|
||||
|
||||
if (minutes < 5) return { text: 'Active', variant: 'default' as const };
|
||||
if (minutes < 30) return { text: 'Idle', variant: 'secondary' as const };
|
||||
return { text: 'Inactive', variant: 'outline' as const };
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Monitor className="h-5 w-5" />
|
||||
Active Sessions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-4">
|
||||
{sessions.map((session) => {
|
||||
const status = getActivityStatus(session.last_activity);
|
||||
return (
|
||||
<div key={session.id} className="border rounded-lg p-4">
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{session.profiles?.username}</span>
|
||||
<Badge variant={status.variant}>
|
||||
<Activity className="h-3 w-3 mr-1" />
|
||||
{status.text}
|
||||
</Badge>
|
||||
{session.is_active && (
|
||||
<Badge variant="default">
|
||||
<Shield className="h-3 w-3 mr-1" />
|
||||
Active
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<p>IP: {session.ip_address}</p>
|
||||
<p>Device: {getDeviceInfo(session.user_agent)}</p>
|
||||
<p>Started: {format(new Date(session.created_at), 'PPp')}</p>
|
||||
<p>Last Activity: {format(new Date(session.last_activity), 'PPp')}</p>
|
||||
</div>
|
||||
</div>
|
||||
{session.is_active && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => terminateSession(session.id)}
|
||||
>
|
||||
<LogOut className="h-4 w-4 mr-1" />
|
||||
Terminate
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{sessions.length === 0 && !loading && (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<Monitor className="h-12 w-12 mx-auto mb-2 opacity-50" />
|
||||
<p>No active sessions</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user