* clear

* feat: Add everything

* fix: Commenht
This commit is contained in:
Prad Nukala
2025-10-03 14:45:52 -04:00
committed by GitHub
parent 43b4a11c06
commit 13e6c3e84d
1935 changed files with 655061 additions and 40058 deletions
@@ -0,0 +1,275 @@
'use client';
import {
AlertTriangle,
CheckCircle,
Clock,
Download,
RefreshCw,
Search,
Shield,
User,
XCircle,
} from 'lucide-react';
import { useState } from 'react';
import { cn } from '../../../lib/utils';
import { Badge } from '../../ui/badge';
import { Button } from '../../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Input } from '../../ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../../ui/table';
/**
* Audit log entry structure
*/
export interface AuditLogEntry {
id: string;
timestamp: Date;
actor: {
id: string;
name: string;
type: 'user' | 'service' | 'system';
};
action: 'grant' | 'revoke' | 'request' | 'deny' | 'expire' | 'attest';
resource: string;
permission: string;
status: 'success' | 'failed' | 'pending';
reason?: string;
metadata?: Record<string, any>;
}
/**
* Props for PermissionAuditLog component
*/
export interface PermissionAuditLogProps {
entries: AuditLogEntry[];
loading?: boolean;
onRefresh?: () => void;
onExport?: () => void;
showFilters?: boolean;
className?: string;
}
/**
* Table display for permission audit trail
*/
export function PermissionAuditLog({
entries,
loading = false,
onRefresh,
onExport,
showFilters = true,
className,
}: PermissionAuditLogProps) {
const [searchQuery, setSearchQuery] = useState('');
const [filterAction, setFilterAction] = useState<string>('all');
const [filterStatus, setFilterStatus] = useState<string>('all');
const filteredEntries = entries.filter((entry) => {
const matchesSearch =
searchQuery === '' ||
entry.actor.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
entry.resource.toLowerCase().includes(searchQuery.toLowerCase()) ||
entry.permission.toLowerCase().includes(searchQuery.toLowerCase());
const matchesAction = filterAction === 'all' || entry.action === filterAction;
const matchesStatus = filterStatus === 'all' || entry.status === filterStatus;
return matchesSearch && matchesAction && matchesStatus;
});
const getActionIcon = (action: AuditLogEntry['action']) => {
switch (action) {
case 'grant':
return <CheckCircle className="h-4 w-4 text-green-500" />;
case 'revoke':
return <XCircle className="h-4 w-4 text-red-500" />;
case 'request':
return <Clock className="h-4 w-4 text-blue-500" />;
case 'deny':
return <XCircle className="h-4 w-4 text-orange-500" />;
case 'expire':
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
case 'attest':
return <Shield className="h-4 w-4 text-purple-500" />;
default:
return null;
}
};
const getStatusBadge = (status: AuditLogEntry['status']) => {
switch (status) {
case 'success':
return (
<Badge variant="default" className="text-xs">
Success
</Badge>
);
case 'failed':
return (
<Badge variant="destructive" className="text-xs">
Failed
</Badge>
);
case 'pending':
return (
<Badge variant="secondary" className="text-xs">
Pending
</Badge>
);
default:
return null;
}
};
const getActorIcon = (type: AuditLogEntry['actor']['type']) => {
switch (type) {
case 'user':
return <User className="h-3 w-3" />;
case 'service':
return <Shield className="h-3 w-3" />;
case 'system':
return <Clock className="h-3 w-3" />;
default:
return null;
}
};
return (
<Card className={cn('', className)}>
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle>Permission Audit Log</CardTitle>
<CardDescription>Track all permission changes and access requests</CardDescription>
</div>
<div className="flex gap-2">
{onRefresh && (
<Button size="sm" variant="outline" onClick={onRefresh} disabled={loading}>
<RefreshCw className={cn('h-4 w-4', loading && 'animate-spin')} />
</Button>
)}
{onExport && (
<Button size="sm" variant="outline" onClick={onExport}>
<Download className="h-4 w-4" />
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent>
{showFilters && (
<div className="flex flex-col sm:flex-row gap-4 mb-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search by actor, resource, or permission..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8"
/>
</div>
</div>
<div className="flex gap-2">
<Select value={filterAction} onValueChange={setFilterAction}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Actions</SelectItem>
<SelectItem value="grant">Grant</SelectItem>
<SelectItem value="revoke">Revoke</SelectItem>
<SelectItem value="request">Request</SelectItem>
<SelectItem value="deny">Deny</SelectItem>
<SelectItem value="expire">Expire</SelectItem>
<SelectItem value="attest">Attest</SelectItem>
</SelectContent>
</Select>
<Select value={filterStatus} onValueChange={setFilterStatus}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Status</SelectItem>
<SelectItem value="success">Success</SelectItem>
<SelectItem value="failed">Failed</SelectItem>
<SelectItem value="pending">Pending</SelectItem>
</SelectContent>
</Select>
</div>
</div>
)}
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-40">Timestamp</TableHead>
<TableHead>Actor</TableHead>
<TableHead>Action</TableHead>
<TableHead>Resource</TableHead>
<TableHead>Permission</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Reason</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8">
<div className="flex items-center justify-center gap-2 text-muted-foreground">
<RefreshCw className="h-4 w-4 animate-spin" />
Loading audit logs...
</div>
</TableCell>
</TableRow>
) : filteredEntries.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-muted-foreground">
No audit log entries found
</TableCell>
</TableRow>
) : (
filteredEntries.map((entry) => (
<TableRow key={entry.id}>
<TableCell className="font-mono text-xs">
{entry.timestamp.toLocaleString()}
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getActorIcon(entry.actor.type)}
<div>
<p className="text-sm font-medium">{entry.actor.name}</p>
<p className="text-xs text-muted-foreground">{entry.actor.type}</p>
</div>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getActionIcon(entry.action)}
<span className="text-sm capitalize">{entry.action}</span>
</div>
</TableCell>
<TableCell className="font-mono text-xs">{entry.resource}</TableCell>
<TableCell>
<Badge variant="outline" className="text-xs">
{entry.permission}
</Badge>
</TableCell>
<TableCell>{getStatusBadge(entry.status)}</TableCell>
<TableCell className="text-right text-xs text-muted-foreground">
{entry.reason || '-'}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
);
}
@@ -0,0 +1,141 @@
'use client';
import { useState } from 'react';
import { cn } from '../../../lib/utils';
import { Badge } from '../../ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Checkbox } from '../../ui/checkbox';
import { Label } from '../../ui/label';
/**
* Permission item structure
*/
export interface Permission {
id: string;
name: string;
description: string;
category: string;
resource?: string;
action?: string;
enabled: boolean;
required?: boolean;
}
/**
* Props for PermissionGrid component
*/
export interface PermissionGridProps {
permissions: Permission[];
onChange?: (permissions: Permission[]) => void;
readOnly?: boolean;
groupByCategory?: boolean;
showDescriptions?: boolean;
className?: string;
}
/**
* Grid display for managing multiple permissions
*/
export function PermissionGrid({
permissions,
onChange,
readOnly = false,
groupByCategory = true,
showDescriptions = true,
className,
}: PermissionGridProps) {
const [selectedPermissions, setSelectedPermissions] = useState<Permission[]>(permissions);
const handlePermissionChange = (permission: Permission, checked: boolean) => {
const updated = selectedPermissions.map((p) =>
p.id === permission.id ? { ...p, enabled: checked } : p
);
setSelectedPermissions(updated);
onChange?.(updated);
};
const groupedPermissions = groupByCategory
? selectedPermissions.reduce<Record<string, Permission[]>>((acc, permission) => {
const category = permission.category;
if (!acc[category]) {
acc[category] = [];
}
acc[category]?.push(permission);
return acc;
}, {})
: { 'All Permissions': selectedPermissions };
return (
<div className={cn('space-y-4', className)}>
{Object.entries(groupedPermissions).map(([category, perms]) => (
<Card key={category}>
<CardHeader>
<CardTitle className="text-base">{category}</CardTitle>
<CardDescription>
{perms.filter((p) => p.enabled).length} of {perms.length} permissions enabled
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{perms.map((permission) => (
<div
key={permission.id}
className={cn(
'flex flex-col space-y-2 rounded-lg border p-3',
permission.enabled && 'bg-accent/50',
permission.required && 'border-primary'
)}
>
<div className="flex items-start space-x-2">
<Checkbox
id={permission.id}
checked={permission.enabled}
onCheckedChange={(checked) =>
handlePermissionChange(permission, checked as boolean)
}
disabled={readOnly || permission.required}
className="mt-0.5"
/>
<div className="flex-1 space-y-1">
<Label
htmlFor={permission.id}
className={cn(
'text-sm font-medium cursor-pointer',
readOnly && 'cursor-not-allowed opacity-60'
)}
>
{permission.name}
{permission.required && (
<Badge variant="secondary" className="ml-2 text-xs">
Required
</Badge>
)}
</Label>
{showDescriptions && (
<p className="text-xs text-muted-foreground">{permission.description}</p>
)}
{(permission.resource || permission.action) && (
<div className="flex gap-2 mt-1">
{permission.resource && (
<Badge variant="outline" className="text-xs">
{permission.resource}
</Badge>
)}
{permission.action && (
<Badge variant="outline" className="text-xs">
{permission.action}
</Badge>
)}
</div>
)}
</div>
</div>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
);
}
@@ -0,0 +1,336 @@
'use client';
import { AlertCircle, Clock, FileText, Info, Key, Shield, User } from 'lucide-react';
import { useState } from 'react';
import { Alert, AlertDescription } from '../../ui/alert';
import { Badge } from '../../ui/badge';
import { Button } from '../../ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
import { Checkbox } from '../../ui/checkbox';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../../ui/dialog';
import { Input } from '../../ui/input';
import { Label } from '../../ui/label';
import { Textarea } from '../../ui/textarea';
// import { cn } from "../../../lib/utils" // Uncomment if needed
/**
* Permission request data structure
*/
export interface PermissionRequestData {
requester: {
id: string;
name: string;
type: 'user' | 'service';
};
permissions: Array<{
resource: string;
action: string;
reason?: string;
}>;
duration?: {
value: number;
unit: 'hours' | 'days' | 'weeks' | 'months';
};
justification: string;
metadata?: Record<string, any>;
}
/**
* Props for PermissionRequest component
*/
export interface PermissionRequestProps {
onSubmit?: (request: PermissionRequestData) => Promise<void>;
availablePermissions?: Array<{
resource: string;
actions: string[];
}>;
requester?: PermissionRequestData['requester'];
maxDuration?: number;
showJustification?: boolean;
className?: string;
}
/**
* Dialog workflow for requesting permissions
*/
export function PermissionRequest({
onSubmit,
availablePermissions = [],
requester,
maxDuration = 30,
showJustification = true,
className,
}: PermissionRequestProps) {
const [open, setOpen] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string>();
const [formData, setFormData] = useState<Partial<PermissionRequestData>>({
requester: requester || { id: '', name: '', type: 'user' },
permissions: [],
justification: '',
});
const [selectedPermissions, setSelectedPermissions] = useState<Set<string>>(new Set());
const handlePermissionToggle = (resource: string, action: string) => {
const key = `${resource}:${action}`;
const newSelected = new Set(selectedPermissions);
if (newSelected.has(key)) {
newSelected.delete(key);
} else {
newSelected.add(key);
}
setSelectedPermissions(newSelected);
// Update form data
const permissions = Array.from(newSelected).map((k) => {
const [res, act] = k.split(':');
return { resource: res || '', action: act || '' };
});
setFormData((prev) => ({ ...prev, permissions }));
};
const handleSubmit = async () => {
if (!formData.permissions || formData.permissions.length === 0) {
setError('Please select at least one permission');
return;
}
if (showJustification && !formData.justification) {
setError('Please provide a justification');
return;
}
setLoading(true);
setError(undefined);
try {
await onSubmit?.(formData as PermissionRequestData);
setOpen(false);
// Reset form
setFormData({
requester: requester || { id: '', name: '', type: 'user' },
permissions: [],
justification: '',
});
setSelectedPermissions(new Set());
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to submit request');
} finally {
setLoading(false);
}
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className={className}>
<Shield className="h-4 w-4 mr-2" />
Request Permissions
</Button>
</DialogTrigger>
<DialogContent className="max-w-2xl max-h-[80vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Request Permissions</DialogTitle>
<DialogDescription>
Select the permissions you need and provide justification for your request
</DialogDescription>
</DialogHeader>
<div className="space-y-6 py-4">
{/* Requester Information */}
{requester && (
<Card>
<CardHeader className="pb-3">
<CardTitle className="text-sm">Requester</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3">
<User className="h-4 w-4 text-muted-foreground" />
<div>
<p className="text-sm font-medium">{requester.name}</p>
<p className="text-xs text-muted-foreground">
{requester.type === 'service' ? 'Service Account' : 'User Account'}
</p>
</div>
</div>
</CardContent>
</Card>
)}
{/* Permission Selection */}
<div className="space-y-3">
<Label>Select Permissions</Label>
{availablePermissions.length === 0 ? (
<Alert>
<Info className="h-4 w-4" />
<AlertDescription>No permissions available to request</AlertDescription>
</Alert>
) : (
<div className="space-y-3">
{availablePermissions.map((perm) => (
<Card key={perm.resource}>
<CardHeader className="pb-3">
<div className="flex items-center gap-2">
<Key className="h-4 w-4 text-muted-foreground" />
<CardTitle className="text-sm">{perm.resource}</CardTitle>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-3">
{perm.actions.map((action) => {
const key = `${perm.resource}:${action}`;
return (
<div key={action} className="flex items-center space-x-2">
<Checkbox
id={key}
checked={selectedPermissions.has(key)}
onCheckedChange={() =>
handlePermissionToggle(perm.resource, action)
}
/>
<Label htmlFor={key} className="text-sm cursor-pointer">
{action}
</Label>
</div>
);
})}
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
{/* Duration Selection */}
<div className="space-y-3">
<Label htmlFor="duration">Duration (Optional)</Label>
<div className="flex gap-2">
<Input
id="duration"
type="number"
min="1"
max={maxDuration}
placeholder="Duration"
value={formData.duration?.value || ''}
onChange={(e) => {
const value = Number.parseInt(e.target.value);
if (value > 0) {
setFormData((prev) => ({
...prev,
duration: {
value,
unit: prev.duration?.unit || 'days',
},
}));
}
}}
className="w-24"
/>
<select
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
value={formData.duration?.unit || 'days'}
onChange={(e) => {
setFormData((prev) => ({
...prev,
duration: {
value: prev.duration?.value || 7,
unit: e.target.value as any,
},
}));
}}
>
<option value="hours">Hours</option>
<option value="days">Days</option>
<option value="weeks">Weeks</option>
<option value="months">Months</option>
</select>
</div>
<p className="text-xs text-muted-foreground">
Leave empty for permanent permissions (subject to approval)
</p>
</div>
{/* Justification */}
{showJustification && (
<div className="space-y-3">
<Label htmlFor="justification">
Justification <span className="text-red-500">*</span>
</Label>
<Textarea
id="justification"
placeholder="Explain why you need these permissions..."
value={formData.justification}
onChange={(e) =>
setFormData((prev) => ({ ...prev, justification: e.target.value }))
}
rows={4}
/>
<p className="text-xs text-muted-foreground">
Provide a clear business justification for this request
</p>
</div>
)}
{/* Error Display */}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
{/* Selected Permissions Summary */}
{selectedPermissions.size > 0 && (
<Card className="bg-muted/50">
<CardHeader className="pb-3">
<CardTitle className="text-sm flex items-center gap-2">
<FileText className="h-4 w-4" />
Request Summary
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-2">
<div className="flex items-center gap-2">
<Clock className="h-3 w-3 text-muted-foreground" />
<span className="text-xs text-muted-foreground">
{formData.duration
? `${formData.duration.value} ${formData.duration.unit}`
: 'Permanent (subject to approval)'}
</span>
</div>
<div className="flex flex-wrap gap-1">
{Array.from(selectedPermissions).map((key) => (
<Badge key={key} variant="secondary" className="text-xs">
{key}
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
)}
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setOpen(false)} disabled={loading}>
Cancel
</Button>
<Button onClick={handleSubmit} disabled={loading || selectedPermissions.size === 0}>
{loading ? 'Submitting...' : 'Submit Request'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,160 @@
'use client';
import { Info } from 'lucide-react';
import { useState } from 'react';
import { cn } from '../../../lib/utils';
import { Badge } from '../../ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from '../../ui/select';
/**
* Permission template structure
*/
export interface PermissionTemplate {
id: string;
name: string;
description: string;
permissions: string[];
category: 'basic' | 'standard' | 'advanced' | 'custom';
icon?: React.ReactNode;
}
/**
* Props for PermissionSelector component
*/
export interface PermissionSelectorProps {
templates: PermissionTemplate[];
value?: string;
onChange?: (templateId: string, template: PermissionTemplate) => void;
showDetails?: boolean;
disabled?: boolean;
className?: string;
}
/**
* Dropdown selector for permission templates with descriptions
*/
export function PermissionSelector({
templates,
value,
onChange,
showDetails = true,
disabled = false,
className,
}: PermissionSelectorProps) {
const [selectedTemplate, setSelectedTemplate] = useState<PermissionTemplate | undefined>(
templates.find((t) => t.id === value)
);
const handleChange = (templateId: string) => {
const template = templates.find((t) => t.id === templateId);
if (template) {
setSelectedTemplate(template);
onChange?.(templateId, template);
}
};
const getCategoryColor = (category: PermissionTemplate['category']) => {
switch (category) {
case 'basic':
return 'bg-blue-100 text-blue-800 dark:bg-blue-900/20 dark:text-blue-400';
case 'standard':
return 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-400';
case 'advanced':
return 'bg-orange-100 text-orange-800 dark:bg-orange-900/20 dark:text-orange-400';
case 'custom':
return 'bg-purple-100 text-purple-800 dark:bg-purple-900/20 dark:text-purple-400';
default:
return '';
}
};
const groupedTemplates = templates.reduce<Record<string, PermissionTemplate[]>>(
(acc, template) => {
const category = template.category;
if (!acc[category]) {
acc[category] = [];
}
acc[category]?.push(template);
return acc;
},
{}
);
return (
<div className={cn('space-y-4', className)}>
<Select value={selectedTemplate?.id || ''} onValueChange={handleChange} disabled={disabled}>
<SelectTrigger className="w-full">
<SelectValue placeholder="Select a permission template" />
</SelectTrigger>
<SelectContent>
{Object.entries(groupedTemplates).map(([category, temps]) => (
<SelectGroup key={category}>
<SelectLabel className="flex items-center gap-2">
<Badge
variant="secondary"
className={cn(
'text-xs',
getCategoryColor(category as PermissionTemplate['category'])
)}
>
{category}
</Badge>
</SelectLabel>
{temps.map((template) => (
<SelectItem key={template.id} value={template.id} className="cursor-pointer">
<div className="flex items-center gap-2">
{template.icon}
<div>
<div className="font-medium">{template.name}</div>
<div className="text-xs text-muted-foreground">{template.description}</div>
</div>
</div>
</SelectItem>
))}
</SelectGroup>
))}
</SelectContent>
</Select>
{showDetails && selectedTemplate && (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="text-base">{selectedTemplate.name}</CardTitle>
<Badge variant="secondary" className={getCategoryColor(selectedTemplate.category)}>
{selectedTemplate.category}
</Badge>
</div>
<CardDescription>{selectedTemplate.description}</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3">
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<Info className="h-4 w-4" />
<span>
This template includes {selectedTemplate.permissions.length} permissions:
</span>
</div>
<div className="flex flex-wrap gap-2">
{selectedTemplate.permissions.map((permission) => (
<Badge key={permission} variant="outline" className="text-xs">
{permission}
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
}
@@ -0,0 +1,276 @@
'use client';
import {
AlertCircle,
CheckCircle,
ChevronDown,
ChevronRight,
Clock,
Copy,
ExternalLink,
Key,
Shield,
} from 'lucide-react';
import { useState } from 'react';
import { cn } from '../../../lib/utils';
import { Badge } from '../../ui/badge';
import { Button } from '../../ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../ui/tabs';
/**
* UCAN capability structure
*/
export interface UCANCapability {
with: string; // Resource identifier
can: string; // Action/permission
nb?: Record<string, any>; // Additional constraints
}
/**
* UCAN token structure
*/
export interface UCANToken {
iss: string; // Issuer DID
aud: string; // Audience DID
exp?: number; // Expiration timestamp
nbf?: number; // Not before timestamp
att: UCANCapability[]; // Attenuations/capabilities
prf?: string[]; // Proof chain
fct?: Record<string, any>; // Facts
}
/**
* Props for UCANViewer component
*/
export interface UCANViewerProps {
token: UCANToken;
showRaw?: boolean;
showProofChain?: boolean;
onCopyToken?: () => void;
onVerify?: () => Promise<boolean>;
className?: string;
}
/**
* Viewer for UCAN token visualization and capability display
*/
export function UCANViewer({
token,
showRaw = true,
showProofChain = true,
onCopyToken,
onVerify,
className,
}: UCANViewerProps) {
const [expandedCapabilities, setExpandedCapabilities] = useState<Set<number>>(new Set());
const [verificationStatus, setVerificationStatus] = useState<
'idle' | 'verifying' | 'valid' | 'invalid'
>('idle');
const toggleCapability = (index: number) => {
const newExpanded = new Set(expandedCapabilities);
if (newExpanded.has(index)) {
newExpanded.delete(index);
} else {
newExpanded.add(index);
}
setExpandedCapabilities(newExpanded);
};
const handleVerify = async () => {
if (!onVerify) return;
setVerificationStatus('verifying');
try {
const isValid = await onVerify();
setVerificationStatus(isValid ? 'valid' : 'invalid');
} catch {
setVerificationStatus('invalid');
}
};
const formatDate = (timestamp?: number) => {
if (!timestamp) return 'Never';
return new Date(timestamp * 1000).toLocaleString();
};
const isExpired = token.exp && token.exp * 1000 < Date.now();
const isNotYetValid = token.nbf && token.nbf * 1000 > Date.now();
return (
<div className={cn('space-y-4', className)}>
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Shield className="h-5 w-5" />
<CardTitle>UCAN Token</CardTitle>
</div>
<div className="flex items-center gap-2">
{isExpired && (
<Badge variant="destructive" className="text-xs">
<AlertCircle className="h-3 w-3 mr-1" />
Expired
</Badge>
)}
{isNotYetValid && (
<Badge variant="secondary" className="text-xs">
<Clock className="h-3 w-3 mr-1" />
Not Yet Valid
</Badge>
)}
{verificationStatus === 'valid' && (
<Badge variant="default" className="text-xs">
<CheckCircle className="h-3 w-3 mr-1" />
Verified
</Badge>
)}
{verificationStatus === 'invalid' && (
<Badge variant="destructive" className="text-xs">
<AlertCircle className="h-3 w-3 mr-1" />
Invalid
</Badge>
)}
</div>
</div>
<CardDescription>
User-Controlled Authorization Network token with delegated capabilities
</CardDescription>
</CardHeader>
<CardContent>
<Tabs defaultValue="details" className="w-full">
<TabsList className="grid w-full grid-cols-3">
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="capabilities">Capabilities ({token.att.length})</TabsTrigger>
{showRaw && <TabsTrigger value="raw">Raw Token</TabsTrigger>}
</TabsList>
<TabsContent value="details" className="space-y-4">
<div className="space-y-3">
<div className="flex items-start gap-2">
<Key className="h-4 w-4 mt-0.5 text-muted-foreground" />
<div className="flex-1">
<p className="text-sm font-medium">Issuer</p>
<code className="text-xs text-muted-foreground break-all">{token.iss}</code>
</div>
</div>
<div className="flex items-start gap-2">
<Key className="h-4 w-4 mt-0.5 text-muted-foreground" />
<div className="flex-1">
<p className="text-sm font-medium">Audience</p>
<code className="text-xs text-muted-foreground break-all">{token.aud}</code>
</div>
</div>
{token.exp && (
<div className="flex items-start gap-2">
<Clock className="h-4 w-4 mt-0.5 text-muted-foreground" />
<div className="flex-1">
<p className="text-sm font-medium">Expires</p>
<p className="text-xs text-muted-foreground">{formatDate(token.exp)}</p>
</div>
</div>
)}
{token.nbf && (
<div className="flex items-start gap-2">
<Clock className="h-4 w-4 mt-0.5 text-muted-foreground" />
<div className="flex-1">
<p className="text-sm font-medium">Not Before</p>
<p className="text-xs text-muted-foreground">{formatDate(token.nbf)}</p>
</div>
</div>
)}
{showProofChain && token.prf && token.prf.length > 0 && (
<div className="flex items-start gap-2">
<ExternalLink className="h-4 w-4 mt-0.5 text-muted-foreground" />
<div className="flex-1">
<p className="text-sm font-medium">Proof Chain</p>
<div className="space-y-1 mt-1">
{token.prf.map((proof, index) => (
<code
key={index}
className="block text-xs text-muted-foreground break-all"
>
{proof}
</code>
))}
</div>
</div>
</div>
)}
</div>
<div className="flex gap-2">
{onVerify && (
<Button
size="sm"
variant="outline"
onClick={handleVerify}
disabled={verificationStatus === 'verifying'}
>
{verificationStatus === 'verifying' ? 'Verifying...' : 'Verify Token'}
</Button>
)}
{onCopyToken && (
<Button size="sm" variant="outline" onClick={onCopyToken}>
<Copy className="h-3 w-3 mr-1" />
Copy Token
</Button>
)}
</div>
</TabsContent>
<TabsContent value="capabilities" className="space-y-3">
{token.att.map((capability, index) => (
<Card key={index} className="border-muted">
<CardHeader
className="cursor-pointer py-3"
onClick={() => toggleCapability(index)}
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{expandedCapabilities.has(index) ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
<Badge variant="outline">{capability.can}</Badge>
<code className="text-xs text-muted-foreground">{capability.with}</code>
</div>
</div>
</CardHeader>
{expandedCapabilities.has(index) && capability.nb && (
<CardContent className="pt-0">
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">Constraints:</p>
<pre className="text-xs bg-muted p-2 rounded overflow-auto">
{JSON.stringify(capability.nb, null, 2)}
</pre>
</div>
</CardContent>
)}
</Card>
))}
{token.att.length === 0 && (
<div className="text-center py-8 text-muted-foreground">
No capabilities defined
</div>
)}
</TabsContent>
{showRaw && (
<TabsContent value="raw" className="space-y-3">
<pre className="text-xs bg-muted p-4 rounded overflow-auto">
{JSON.stringify(token, null, 2)}
</pre>
</TabsContent>
)}
</Tabs>
</CardContent>
</Card>
</div>
);
}
@@ -0,0 +1,5 @@
export * from './PermissionGrid';
export * from './PermissionSelector';
export * from './UCANViewer';
export * from './PermissionAuditLog';
export * from './PermissionRequest';