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:
Claude
2025-11-14 00:46:35 +00:00
parent bb3d9aeb29
commit c48ded7ff2
206 changed files with 502 additions and 4 deletions
@@ -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>
);
}