- Added main application entry point (app.jsx) and bootstrap file for React setup. - Created Wiki index page (Index.jsx) for documentation and file management. - Implemented utility functions for formatting and status handling in utils.js. - Set up Blade template (app.blade.php) for Inertia.js integration. - Defined API routes for metrics and terminal access in api.php. - Established console commands for scheduled tasks in console.php. - Created web routes for various application features including authentication, hypervisors, containers, cloud accounts, security, backups, and wiki management in web.php. - Developed setup scripts for Windows (setup.ps1) and Linux (setup.sh) environments. - Configured Tailwind CSS for styling with custom themes and animations in tailwind.config.js. - Set up Vite for asset bundling and development server in vite.config.js.
162 lines
9.0 KiB
JavaScript
162 lines
9.0 KiB
JavaScript
import AppLayout from '@/Layouts/AppLayout';
|
|
import { Head } from '@inertiajs/react';
|
|
import { KeyRound, Lock, Certificate, Key, Plus, Eye, EyeOff, Copy, AlertCircle } from 'lucide-react';
|
|
import { GlassCard, StatCard } from '@/Components/ui/GlassCard';
|
|
import { StatusBadge } from '@/Components/ui/StatusBadge';
|
|
import { Button } from '@/Components/ui/Button';
|
|
import { DataTable } from '@/Components/ui/DataTable';
|
|
import { timeAgo } from '@/lib/utils';
|
|
import { useState } from 'react';
|
|
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
export default function IdentityIndex({ passwords, certificates, ssh_keys, stats }) {
|
|
const [activeTab, setActiveTab] = useState('passwords');
|
|
|
|
const passwordColumns = [
|
|
{ header: 'Title', accessor: 'title', render: row => (
|
|
<div>
|
|
<p className="text-sm text-white font-medium">{row.title}</p>
|
|
{row.url && <p className="text-xs text-white/30 truncate max-w-48">{row.url}</p>}
|
|
</div>
|
|
)},
|
|
{ header: 'Username', accessor: 'username', render: row => (
|
|
<span className="font-mono text-xs text-white/60">{row.username ?? '—'}</span>
|
|
)},
|
|
{ header: 'Category', accessor: 'category', render: row => (
|
|
row.category ? <span className="text-xs px-2 py-0.5 rounded bg-white/5 text-white/60">{row.category}</span> : null
|
|
)},
|
|
{ header: 'Expires', accessor: 'expires_at', render: row => (
|
|
row.expires_at
|
|
? <span className={`text-xs ${new Date(row.expires_at) < new Date() ? 'text-red-400' : 'text-white/40'}`}>{timeAgo(row.expires_at)}</span>
|
|
: <span className="text-xs text-white/20">Never</span>
|
|
)},
|
|
{ header: '', key: 'actions', render: row => (
|
|
<div className="flex gap-1">
|
|
<Button size="sm" variant="ghost" icon={Eye} onClick={() => {}}>Reveal</Button>
|
|
<Button size="sm" variant="ghost" icon={Copy} onClick={() => {}}>Copy</Button>
|
|
</div>
|
|
)},
|
|
];
|
|
|
|
const certColumns = [
|
|
{ header: 'Domain', accessor: 'domain', render: row => (
|
|
<div>
|
|
<p className="text-sm text-white font-medium font-mono">{row.domain}</p>
|
|
<p className="text-xs text-white/30">{row.issuer ?? 'Self-signed'}</p>
|
|
</div>
|
|
)},
|
|
{ header: 'Type', accessor: 'type', render: row => (
|
|
<span className="text-xs px-2 py-0.5 rounded bg-white/5 text-white/60 capitalize">{row.type?.replace('-', ' ')}</span>
|
|
)},
|
|
{ header: 'Expires', accessor: 'expires_at', render: row => {
|
|
const daysLeft = Math.ceil((new Date(row.expires_at) - new Date()) / 86400000);
|
|
return (
|
|
<div>
|
|
<p className={`text-sm font-medium ${daysLeft < 30 ? 'text-red-400' : daysLeft < 60 ? 'text-yellow-400' : 'text-white/70'}`}>
|
|
{daysLeft > 0 ? `${daysLeft}d left` : 'Expired'}
|
|
</p>
|
|
<p className="text-xs text-white/30">{new Date(row.expires_at).toLocaleDateString()}</p>
|
|
</div>
|
|
);
|
|
}},
|
|
{ header: 'Status', accessor: 'status', render: row => <StatusBadge status={row.status} /> },
|
|
{ header: '', key: 'actions', render: row => (
|
|
<Button size="sm" variant="secondary">Download</Button>
|
|
)},
|
|
];
|
|
|
|
return (
|
|
<AppLayout title="Identity & Secrets">
|
|
<Head title="Identity & Secrets" />
|
|
<div className="space-y-5">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h2 className="text-xl font-bold text-white">Identity & Secrets</h2>
|
|
<p className="text-sm text-white/40">Passwords, certificates, SSH keys, and secrets</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
|
<StatCard title="Passwords" value={stats?.passwords ?? 0} icon={Lock} color="cyan" />
|
|
<StatCard title="Certificates" value={stats?.certs ?? 0} icon={KeyRound} color="green" />
|
|
<StatCard title="Expiring Soon" value={stats?.expiring ?? 0} icon={AlertCircle} color="red" />
|
|
<StatCard title="SSH Keys" value={stats?.ssh_keys ?? 0} icon={Key} color="purple" />
|
|
</div>
|
|
|
|
{/* Tabs */}
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList className="flex gap-1 p-1 bg-white/5 rounded-xl w-fit border border-white/10 mb-4">
|
|
{[
|
|
{ value: 'passwords', label: 'Passwords', icon: Lock },
|
|
{ value: 'certificates', label: 'Certificates', icon: KeyRound },
|
|
{ value: 'ssh-keys', label: 'SSH Keys', icon: Key },
|
|
].map(t => {
|
|
const Icon = t.icon;
|
|
return (
|
|
<TabsTrigger key={t.value} value={t.value}
|
|
className={cn(
|
|
'flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all',
|
|
activeTab === t.value
|
|
? 'bg-white/10 text-white'
|
|
: 'text-white/50 hover:text-white'
|
|
)}>
|
|
<Icon className="w-3.5 h-3.5" />
|
|
{t.label}
|
|
</TabsTrigger>
|
|
);
|
|
})}
|
|
</TabsList>
|
|
|
|
<TabsContent value="passwords">
|
|
<GlassCard className="p-5">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-sm font-semibold text-white">Password Vault</h3>
|
|
<Button icon={Plus} size="sm">Add Password</Button>
|
|
</div>
|
|
<DataTable columns={passwordColumns} data={passwords ?? []}
|
|
emptyMessage="No passwords stored. Add your first entry." />
|
|
</GlassCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="certificates">
|
|
<GlassCard className="p-5">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-sm font-semibold text-white">SSL/TLS Certificates</h3>
|
|
<div className="flex gap-2">
|
|
<Button variant="secondary" size="sm">Generate Self-Signed</Button>
|
|
<Button icon={Plus} size="sm">Import Cert</Button>
|
|
</div>
|
|
</div>
|
|
<DataTable columns={certColumns} data={certificates ?? []}
|
|
emptyMessage="No certificates. Import or generate your first certificate." />
|
|
</GlassCard>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="ssh-keys">
|
|
<GlassCard className="p-5">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-sm font-semibold text-white">SSH Keys</h3>
|
|
<div className="flex gap-2">
|
|
<Button variant="secondary" size="sm">Generate Key Pair</Button>
|
|
<Button icon={Plus} size="sm">Import Key</Button>
|
|
</div>
|
|
</div>
|
|
<DataTable
|
|
columns={[
|
|
{ header: 'Name', accessor: 'name', render: r => <p className="text-sm text-white">{r.name}</p> },
|
|
{ header: 'Algorithm', accessor: 'algorithm', render: r => <span className="font-mono text-xs text-white/60">{r.algorithm?.toUpperCase()} {r.key_size}</span> },
|
|
{ header: 'Fingerprint', accessor: 'fingerprint', render: r => <span className="font-mono text-xs text-white/40 truncate max-w-40 block">{r.fingerprint}</span> },
|
|
{ header: '', key: 'a', render: r => <Button size="sm" variant="ghost" icon={Copy}>Copy Public Key</Button> },
|
|
]}
|
|
data={ssh_keys ?? []}
|
|
emptyMessage="No SSH keys. Generate or import a key pair."
|
|
/>
|
|
</GlassCard>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
</AppLayout>
|
|
);
|
|
}
|