feat: initialize Infra Manager with React, Tailwind CSS, and Inertia.js

- 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.
This commit is contained in:
I-Fun
2026-04-07 20:29:32 +07:00
commit c649f055d8
126 changed files with 8122 additions and 0 deletions

View File

@@ -0,0 +1,93 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, router } from '@inertiajs/react';
import { Settings, Users, Activity, Shield, Database, Globe, Server, ChevronRight } from 'lucide-react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { motion } from 'framer-motion';
import { timeAgo } from '@/lib/utils';
const adminSections = [
{ title: 'General Settings', href: '/admin/settings', icon: Settings, desc: 'App name, theme, session timeout' },
{ title: 'LDAP / SSO', href: '/admin/ldap', icon: Globe, desc: 'Configure LDAP/AD authentication' },
{ title: 'Users', href: '/acl/users', icon: Users, desc: 'Manage user accounts' },
{ title: 'Roles & ACL', href: '/acl/roles', icon: Shield, desc: 'Roles and permission management' },
{ title: 'Audit Log', href: '/admin/audit-log', icon: Activity, desc: 'User activity and system events' },
{ title: 'System Health', href: '/admin/system-health',icon: Server, desc: 'Database, queues, storage status' },
];
export default function AdminIndex({ stats, recent_activity }) {
return (
<AppLayout title="Administration">
<Head title="Administration" />
<div className="space-y-5">
<div>
<h2 className="text-xl font-bold text-white">Administration</h2>
<p className="text-sm text-white/40">System settings, users, and maintenance</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-3">
{adminSections.map(s => {
const Icon = s.icon;
return (
<a key={s.href} href={s.href}>
<GlassCard className="p-5 cursor-pointer group" hover glow="cyan">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-white/5 border border-white/10 flex items-center justify-center group-hover:border-cyan-500/30 group-hover:bg-cyan-500/10 transition-all">
<Icon className="w-5 h-5 text-white/50 group-hover:text-cyan-400 transition-colors" />
</div>
<div>
<p className="text-sm font-medium text-white group-hover:text-cyan-300 transition-colors">{s.title}</p>
<p className="text-xs text-white/30">{s.desc}</p>
</div>
</div>
<ChevronRight className="w-4 h-4 text-white/20 group-hover:text-white/50 transition-colors" />
</div>
</GlassCard>
</a>
);
})}
</div>
{/* System stats */}
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
{[
{ label: 'Total Users', value: stats?.users ?? 0 },
{ label: 'Active Sessions', value: stats?.sessions ?? 0 },
{ label: 'Queue Jobs', value: stats?.queued ?? 0 },
{ label: 'DB Size', value: stats?.db_size ?? '—' },
].map(s => (
<GlassCard key={s.label} className="p-4 text-center">
<p className="text-2xl font-bold text-white">{s.value}</p>
<p className="text-xs text-white/40 mt-0.5">{s.label}</p>
</GlassCard>
))}
</div>
{/* Recent activity */}
<GlassCard className="p-5">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-semibold text-white">Recent Activity</h3>
<a href="/admin/audit-log"><Button size="sm" variant="ghost">View All →</Button></a>
</div>
<div className="space-y-2">
{(recent_activity ?? []).map((log, i) => (
<div key={i} className="flex items-start gap-3 py-2 border-b border-white/[0.05] last:border-0">
<div className="w-7 h-7 rounded-full bg-white/5 border border-white/10 flex items-center justify-center text-xs shrink-0">
{log.causer?.name?.charAt(0)?.toUpperCase() ?? '?'}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm text-white/80">{log.description}</p>
<p className="text-xs text-white/30">{log.causer?.name ?? 'System'} · {timeAgo(log.created_at)}</p>
</div>
</div>
))}
{(!recent_activity || recent_activity.length === 0) && (
<p className="text-sm text-white/30 text-center py-4">No recent activity</p>
)}
</div>
</GlassCard>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,146 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, useForm, router } from '@inertiajs/react';
import { useState } from 'react';
import { Globe, CheckCircle, XCircle, RefreshCw } from 'lucide-react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { cn } from '@/lib/utils';
import { motion } from 'framer-motion';
function Input({ className, ...props }) {
return <input className={cn('w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/60 transition-all', className)} {...props} />;
}
function Field({ label, desc, children }) {
return (
<div className="flex items-start justify-between gap-8 py-4 border-b border-white/[0.06] last:border-0">
<div className="flex-1">
<p className="text-sm font-medium text-white">{label}</p>
{desc && <p className="text-xs text-white/40 mt-0.5">{desc}</p>}
</div>
<div className="w-72 shrink-0">{children}</div>
</div>
);
}
export default function LdapConfig({ config }) {
const { data, setData, put, processing } = useForm({
host: config?.host ?? '',
port: config?.port ?? 389,
use_ssl: config?.use_ssl ?? false,
use_tls: config?.use_tls ?? false,
base_dn: config?.base_dn ?? '',
bind_dn: config?.bind_dn ?? '',
bind_password: '',
user_search_base: config?.user_search_base ?? '',
user_filter: config?.user_filter ?? '(sAMAccountName={username})',
admin_group_dn: config?.admin_group_dn ?? '',
is_active: config?.is_active ?? false,
});
const [testResult, setTestResult] = useState(null);
const [testing, setTesting] = useState(false);
const [syncing, setSyncing] = useState(false);
const test = async () => {
setTesting(true);
setTestResult(null);
try {
const res = await fetch('/admin/ldap/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content },
body: JSON.stringify(data),
});
setTestResult(await res.json());
} catch (e) {
setTestResult({ success: false, message: e.message });
} finally {
setTesting(false);
}
};
const sync = () => {
setSyncing(true);
router.post('/admin/ldap/sync', {}, { onFinish: () => setSyncing(false) });
};
return (
<AppLayout title="LDAP Configuration">
<Head title="LDAP" />
<div className="max-w-3xl space-y-5">
<div className="flex items-center justify-between">
<div>
<h2 className="text-xl font-bold text-white">LDAP / Active Directory</h2>
<p className="text-sm text-white/40">Integrate with your directory service for SSO</p>
</div>
<div className="flex gap-2">
<Button variant="secondary" icon={RefreshCw} loading={syncing} onClick={sync}>Sync Users</Button>
</div>
</div>
<form onSubmit={e => { e.preventDefault(); put('/admin/ldap'); }}>
<div className="space-y-4">
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Connection</h3>
<Field label="Enable LDAP" desc="Allow users to authenticate via LDAP">
<label className="flex items-center gap-3 cursor-pointer">
<input type="checkbox" checked={data.is_active} onChange={e => setData('is_active', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0" />
<span className="text-sm text-white/70">Enabled</span>
</label>
</Field>
<Field label="LDAP Host">
<Input value={data.host} onChange={e => setData('host', e.target.value)} placeholder="ldap.company.com" />
</Field>
<Field label="Port">
<Input type="number" value={data.port} onChange={e => setData('port', parseInt(e.target.value))} />
</Field>
<Field label="Encryption">
<div className="flex gap-3">
{[['use_ssl', 'LDAPS (SSL)'], ['use_tls', 'STARTTLS']].map(([k, l]) => (
<label key={k} className="flex items-center gap-2 cursor-pointer">
<input type="checkbox" checked={data[k]} onChange={e => setData(k, e.target.checked)}
className="w-3.5 h-3.5 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0" />
<span className="text-xs text-white/60">{l}</span>
</label>
))}
</div>
</Field>
</GlassCard>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Bind / Authentication</h3>
<Field label="Base DN"><Input value={data.base_dn} onChange={e => setData('base_dn', e.target.value)} placeholder="DC=company,DC=com" /></Field>
<Field label="Bind DN"><Input value={data.bind_dn} onChange={e => setData('bind_dn', e.target.value)} placeholder="CN=ldap-reader,CN=Users,DC=company,DC=com" /></Field>
<Field label="Bind Password"><Input type="password" value={data.bind_password} onChange={e => setData('bind_password', e.target.value)} placeholder="Leave blank to keep existing" /></Field>
</GlassCard>
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">User Search</h3>
<Field label="User Search Base"><Input value={data.user_search_base} onChange={e => setData('user_search_base', e.target.value)} placeholder="CN=Users,DC=company,DC=com" /></Field>
<Field label="User Filter" desc="Use {username} as placeholder">
<Input value={data.user_filter} onChange={e => setData('user_filter', e.target.value)} />
</Field>
<Field label="Admin Group DN" desc="Members of this group get admin role">
<Input value={data.admin_group_dn} onChange={e => setData('admin_group_dn', e.target.value)} placeholder="CN=Infra-Admins,CN=Groups,..." />
</Field>
</GlassCard>
{testResult && (
<motion.div initial={{ opacity: 0, y: -10 }} animate={{ opacity: 1, y: 0 }}
className={cn('flex items-center gap-3 p-3 rounded-xl border text-sm',
testResult.success ? 'bg-green-500/10 border-green-500/30 text-green-300' : 'bg-red-500/10 border-red-500/30 text-red-300'
)}>
{testResult.success ? <CheckCircle className="w-4 h-4" /> : <XCircle className="w-4 h-4" />}
{testResult.message}
</motion.div>
)}
<div className="flex items-center justify-between pt-2">
<Button type="button" variant="secondary" loading={testing} onClick={test}>Test Connection</Button>
<Button type="submit" loading={processing}>Save Configuration</Button>
</div>
</div>
</form>
</div>
</AppLayout>
);
}

View File

@@ -0,0 +1,115 @@
import AppLayout from '@/Layouts/AppLayout';
import { Head, useForm } from '@inertiajs/react';
import { GlassCard } from '@/Components/ui/GlassCard';
import { Button } from '@/Components/ui/Button';
import { cn } from '@/lib/utils';
function Field({ label, desc, error, children }) {
return (
<div className="flex items-start justify-between gap-8 py-4 border-b border-white/[0.06] last:border-0">
<div className="flex-1">
<p className="text-sm font-medium text-white">{label}</p>
{desc && <p className="text-xs text-white/40 mt-0.5">{desc}</p>}
{error && <p className="text-xs text-red-400 mt-1">{error}</p>}
</div>
<div className="w-72 shrink-0">{children}</div>
</div>
);
}
function Input({ className, ...props }) {
return (
<input className={cn(
'w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/20',
'focus:outline-none focus:border-cyan-500/60 transition-all',
className
)} {...props} />
);
}
function Select({ className, children, ...props }) {
return (
<select className={cn(
'w-full px-3 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white',
'focus:outline-none focus:border-cyan-500/60 transition-all',
'[&>option]:bg-[#0f1f35]',
className
)} {...props}>{children}</select>
);
}
export default function AdminSettings({ settings }) {
const { data, setData, put, processing, errors } = useForm({
'app.name': settings?.['app.name'] ?? 'Infra Manager',
'app.theme': settings?.['app.theme'] ?? 'dark',
'security.session_timeout': settings?.['security.session_timeout'] ?? 480,
'security.mfa_required': settings?.['security.mfa_required'] ?? false,
'mail.driver': settings?.['mail.driver'] ?? 'log',
'mail.from': settings?.['mail.from'] ?? '',
});
return (
<AppLayout title="Settings">
<Head title="Settings" />
<div className="max-w-3xl space-y-5">
<div>
<h2 className="text-xl font-bold text-white">General Settings</h2>
<p className="text-sm text-white/40">Application-wide configuration</p>
</div>
<form onSubmit={e => { e.preventDefault(); put('/admin/settings'); }}>
<div className="space-y-4">
{/* App */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Application</h3>
<Field label="App Name" desc="Displayed in the browser tab and sidebar">
<Input value={data['app.name']} onChange={e => setData('app.name', e.target.value)} />
</Field>
<Field label="Theme" desc="Interface color scheme">
<Select value={data['app.theme']} onChange={e => setData('app.theme', e.target.value)}>
<option value="dark">Dark (Default)</option>
<option value="darker">Darker</option>
</Select>
</Field>
</GlassCard>
{/* Security */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Security</h3>
<Field label="Session Timeout" desc="Minutes before inactive users are logged out">
<Input type="number" value={data['security.session_timeout']} onChange={e => setData('security.session_timeout', e.target.value)} />
</Field>
<Field label="Require MFA" desc="Force MFA for all users">
<label className="flex items-center gap-3 cursor-pointer">
<input type="checkbox" checked={data['security.mfa_required']}
onChange={e => setData('security.mfa_required', e.target.checked)}
className="w-4 h-4 rounded border-white/20 bg-white/5 text-cyan-500 focus:ring-0" />
<span className="text-sm text-white/70">Enabled</span>
</label>
</Field>
</GlassCard>
{/* Mail */}
<GlassCard className="p-5">
<h3 className="text-sm font-semibold text-white/60 uppercase tracking-wider mb-1">Email / Notifications</h3>
<Field label="Mail Driver">
<Select value={data['mail.driver']} onChange={e => setData('mail.driver', e.target.value)}>
<option value="log">Log (dev only)</option>
<option value="smtp">SMTP</option>
<option value="sendmail">Sendmail</option>
</Select>
</Field>
<Field label="From Address" desc="Default sender email for notifications">
<Input type="email" value={data['mail.from']} onChange={e => setData('mail.from', e.target.value)} placeholder="noreply@example.com" />
</Field>
</GlassCard>
<div className="flex justify-end">
<Button type="submit" loading={processing}>Save Settings</Button>
</div>
</div>
</form>
</div>
</AppLayout>
);
}