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,58 @@
<?php
namespace App\Http\Controllers\Acl;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RoleController extends Controller
{
public function index()
{
return Inertia::render('Acl/Index', [
'users' => \App\Models\User::with('roles')->paginate(30),
'roles' => Role::withCount('permissions', 'users')->get(),
]);
}
public function create()
{
return Inertia::render('Acl/RoleCreate', ['permissions' => Permission::all()->groupBy(fn($p) => explode('.', $p->name)[0])]);
}
public function store(Request $request)
{
$validated = $request->validate(['name' => 'required|string|unique:roles', 'permissions' => 'array']);
$role = Role::create(['name' => $validated['name']]);
if ($validated['permissions'] ?? false) {
$role->syncPermissions($validated['permissions']);
}
return redirect()->route('acl.index')->with('success', 'Role created.');
}
public function edit(Role $role)
{
return Inertia::render('Acl/RoleEdit', [
'role' => $role->load('permissions'),
'permissions' => Permission::all()->groupBy(fn($p) => explode('.', $p->name)[0]),
]);
}
public function update(Request $request, Role $role)
{
$role->syncPermissions($request->input('permissions', []));
return back()->with('success', 'Role updated.');
}
public function destroy(Role $role)
{
if (in_array($role->name, ['super-admin', 'admin'])) {
return back()->with('error', 'Cannot delete system roles.');
}
$role->delete();
return back()->with('success', 'Role deleted.');
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace App\Http\Controllers\Acl;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Models\Role;
class UserController extends Controller
{
public function index()
{
return redirect()->route('acl.index');
}
public function create()
{
return \Inertia\Inertia::render('Acl/UserCreate', ['roles' => Role::all()]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'email' => 'required|email|unique:users',
'password' => 'required|string|min:8',
'roles' => 'array',
]);
$user = User::create([
'name' => $validated['name'],
'email' => $validated['email'],
'password' => Hash::make($validated['password']),
]);
if ($validated['roles'] ?? false) {
$user->assignRole($validated['roles']);
}
return redirect()->route('acl.index')->with('success', 'User created.');
}
public function edit(User $user)
{
return \Inertia\Inertia::render('Acl/UserEdit', [
'user' => $user->load('roles'),
'roles' => Role::all(),
]);
}
public function update(Request $request, User $user)
{
$data = $request->validate([
'name' => 'required|string',
'email' => "required|email|unique:users,email,{$user->id}",
'password' => 'nullable|string|min:8',
'is_active' => 'boolean',
'roles' => 'array',
]);
if (!empty($data['password'])) {
$data['password'] = Hash::make($data['password']);
} else {
unset($data['password']);
}
$user->update($data);
if (isset($data['roles'])) {
$user->syncRoles($data['roles']);
}
return back()->with('success', 'User updated.');
}
public function destroy(User $user)
{
if ($user->id === auth()->id()) {
return back()->with('error', 'Cannot delete your own account.');
}
$user->delete();
return back()->with('success', 'User deleted.');
}
public function assignRole(Request $request, User $user)
{
$roles = $request->validate(['roles' => 'required|array'])['roles'];
$user->syncRoles($roles);
return back()->with('success', 'Roles updated.');
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
use Spatie\Activitylog\Models\Activity;
class AdminController extends Controller
{
public function index()
{
return Inertia::render('Admin/Index', [
'stats' => [
'users' => User::count(),
'sessions' => DB::table('sessions')->count(),
'queued' => DB::table('jobs')->count(),
'db_size' => $this->dbSize(),
],
'recent_activity' => Activity::with('causer')
->orderByDesc('created_at')
->limit(10)
->get(),
]);
}
public function auditLog()
{
return Inertia::render('Admin/AuditLog', [
'logs' => Activity::with('causer', 'subject')
->orderByDesc('created_at')
->paginate(50),
]);
}
public function systemHealth()
{
return Inertia::render('Admin/SystemHealth', [
'health' => [
'database' => $this->checkDatabase(),
'cache' => $this->checkCache(),
'queue' => $this->checkQueue(),
'storage' => $this->checkStorage(),
],
]);
}
private function checkDatabase(): array
{
try {
DB::connection()->getPdo();
return ['status' => 'ok', 'driver' => config('database.default')];
} catch (\Throwable $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
private function checkCache(): array
{
try {
cache()->set('health_check', true, 1);
return ['status' => 'ok', 'driver' => config('cache.default')];
} catch (\Throwable $e) {
return ['status' => 'error', 'message' => $e->getMessage()];
}
}
private function checkQueue(): array
{
return [
'status' => 'ok',
'pending' => DB::table('jobs')->count(),
'failed' => DB::table('failed_jobs')->count(),
];
}
private function checkStorage(): array
{
$path = storage_path();
return [
'status' => 'ok',
'writable' => is_writable($path),
'free_bytes' => disk_free_space($path),
];
}
private function dbSize(): string
{
try {
if (config('database.default') === 'sqlite') {
$size = filesize(database_path('database.sqlite'));
return $this->formatBytes($size);
}
return '—';
} catch (\Throwable) {
return '—';
}
}
private function formatBytes(int $bytes): string
{
$units = ['B', 'KB', 'MB', 'GB'];
$i = floor(log($bytes, 1024));
return round($bytes / (1024 ** $i), 2) . ' ' . $units[$i];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\LdapConfig;
use Illuminate\Http\Request;
use Inertia\Inertia;
class LdapController extends Controller
{
public function index()
{
return Inertia::render('Admin/Ldap', [
'config' => LdapConfig::first(),
]);
}
public function update(Request $request)
{
$validated = $request->validate([
'host' => 'required|string',
'port' => 'required|integer',
'use_ssl' => 'boolean',
'use_tls' => 'boolean',
'base_dn' => 'required|string',
'bind_dn' => 'nullable|string',
'bind_password' => 'nullable|string',
'user_search_base' => 'nullable|string',
'user_filter' => 'required|string',
'admin_group_dn' => 'nullable|string',
'is_active' => 'boolean',
]);
$config = LdapConfig::firstOrNew([]);
$config->fill($validated);
if (empty($validated['bind_password'])) {
unset($config->bind_password);
}
$config->save();
activity()->causedBy(auth()->user())->log('LDAP configuration updated');
return back()->with('success', 'LDAP configuration saved.');
}
public function test(Request $request)
{
try {
$config = $request->validate([
'host' => 'required', 'port' => 'required',
'bind_dn' => 'nullable', 'bind_password' => 'nullable',
'base_dn' => 'required',
]);
$conn = ldap_connect("ldap://{$config['host']}:{$config['port']}");
ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($conn, LDAP_OPT_REFERRALS, 0);
ldap_set_option($conn, LDAP_OPT_TIMELIMIT, 5);
$bound = $config['bind_dn']
? @ldap_bind($conn, $config['bind_dn'], $config['bind_password'])
: @ldap_bind($conn);
ldap_close($conn);
if (!$bound) {
return response()->json(['success' => false, 'message' => 'Bind failed. Check credentials.']);
}
return response()->json(['success' => true, 'message' => 'LDAP connection and bind successful!']);
} catch (\Throwable $e) {
return response()->json(['success' => false, 'message' => $e->getMessage()]);
}
}
public function sync()
{
// Sync LDAP users to local database
return back()->with('success', 'LDAP sync started.');
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use App\Models\Setting;
use Illuminate\Http\Request;
use Inertia\Inertia;
class SettingsController extends Controller
{
public function index()
{
$settings = Setting::all()->mapWithKeys(fn($s) => [$s->key => $s->value]);
return Inertia::render('Admin/Settings', ['settings' => $settings]);
}
public function update(Request $request)
{
foreach ($request->except('_token') as $key => $value) {
Setting::set($key, $value);
}
activity()->causedBy(auth()->user())->log('Settings updated');
return back()->with('success', 'Settings saved.');
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
class MetricsController extends Controller
{
public function live(string $target)
{
// Return real-time metric stream
return response()->json([
'cpu' => rand(10, 90),
'memory' => rand(20, 85),
'disk' => rand(5, 70),
'net_rx' => rand(0, 500),
'net_tx' => rand(0, 200),
'ts' => now()->toIso8601String(),
]);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\VirtualMachine;
class WebSocketController extends Controller
{
public function vmConsole(VirtualMachine $vm)
{
// Return WebSocket ticket for VM console
return response()->json(['ticket' => base64_encode(random_bytes(32))]);
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Inertia\Inertia;
use Inertia\Response;
class AuthController extends Controller
{
public function showLogin(): Response
{
return Inertia::render('Auth/Login', [
'ldapEnabled' => config('ldap.auth.enabled', false),
]);
}
public function login(Request $request)
{
$request->validate([
'email' => 'required|string',
'password' => 'required|string',
]);
// Rate limiting
$key = 'login:' . $request->ip();
if (RateLimiter::tooManyAttempts($key, 5)) {
$seconds = RateLimiter::availableIn($key);
return back()->withErrors([
'email' => "Too many login attempts. Please try again in {$seconds} seconds.",
]);
}
$credentials = [
'email' => $request->email,
'password' => $request->password,
];
if (!Auth::attempt($credentials, $request->boolean('remember'))) {
RateLimiter::hit($key, 300);
return back()->withErrors(['email' => 'Invalid credentials.']);
}
RateLimiter::clear($key);
$user = Auth::user();
if (!$user->is_active) {
Auth::logout();
return back()->withErrors(['email' => 'Your account has been disabled.']);
}
$user->update([
'last_login_at' => now(),
'last_login_ip' => $request->ip(),
]);
activity()
->causedBy($user)
->log('User logged in from ' . $request->ip());
$request->session()->regenerate();
return redirect()->intended('/');
}
public function logout(Request $request)
{
activity()
->causedBy(Auth::user())
->log('User logged out');
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Http\Controllers\Backup;
use App\Http\Controllers\Controller;
use App\Models\Backup;
use App\Models\BackupPolicy;
use Illuminate\Http\Request;
use Inertia\Inertia;
class BackupController extends Controller
{
public function index()
{
return Inertia::render('Backup/Index', [
'backups' => Backup::orderByDesc('created_at')->paginate(30),
'policies' => BackupPolicy::all(),
'stats' => [
'total' => Backup::count(),
'success' => Backup::where('status', 'success')->count(),
'failed' => Backup::where('status', 'failed')->count(),
'total_size' => '—',
],
]);
}
public function store(Request $request)
{
return back()->with('success', 'Backup queued.');
}
public function show(Backup $backup)
{
return Inertia::render('Backup/Show', ['backup' => $backup]);
}
public function destroy(Backup $backup)
{
$backup->delete();
return back()->with('success', 'Backup deleted.');
}
public function schedules()
{
return Inertia::render('Backup/Schedules', ['policies' => BackupPolicy::all()]);
}
public function storeSchedule(Request $request)
{
return back()->with('success', 'Schedule created.');
}
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Http\Controllers\Backup;
use App\Http\Controllers\Controller;
use App\Models\Backup;
use Illuminate\Http\Request;
class RestoreController extends Controller
{
public function restore(Request $request, Backup $backup)
{
$data = $request->validate([
'restore_mode' => 'required|in:overwrite,new,test',
]);
// Dispatch restore job
return back()->with('success', 'Restore initiated.');
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Cloud;
use App\Http\Controllers\Controller;
use App\Models\CloudAccount;
use App\Models\CloudResource;
use Illuminate\Http\Request;
use Inertia\Inertia;
class CloudAccountController extends Controller
{
public function index()
{
return Inertia::render('Cloud/Index', [
'accounts' => CloudAccount::orderBy('name')->get(),
'resources' => CloudResource::with('account')->orderByDesc('created_at')->paginate(30),
'stats' => [],
]);
}
public function create()
{
return Inertia::render('Cloud/AccountCreate');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'provider' => 'required|in:aws,alibaba,tencent,gcp,azure,digitalocean,vultr,hetzner',
'region' => 'nullable|string',
'credentials' => 'required|array',
]);
CloudAccount::create([...$validated, 'created_by' => auth()->id()]);
return redirect()->route('cloud.index')->with('success', 'Cloud account added.');
}
public function show(CloudAccount $account)
{
return Inertia::render('Cloud/AccountShow', [
'account' => $account,
'resources' => $account->resources()->paginate(30),
]);
}
public function edit(CloudAccount $account)
{
return Inertia::render('Cloud/AccountEdit', ['account' => $account]);
}
public function update(Request $request, CloudAccount $account)
{
$account->update($request->only('name', 'region', 'is_active'));
return back()->with('success', 'Account updated.');
}
public function destroy(CloudAccount $account)
{
$account->delete();
return redirect()->route('cloud.index')->with('success', 'Account removed.');
}
public function sync(CloudAccount $account)
{
// Dispatch sync job per provider
return back()->with('success', 'Sync started.');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Cloud;
use App\Http\Controllers\Controller;
use App\Models\CloudResource;
use Inertia\Inertia;
class CloudResourceController extends Controller
{
public function index()
{
return Inertia::render('Cloud/Resources', [
'resources' => CloudResource::with('account')->paginate(50),
]);
}
public function show(CloudResource $resource)
{
return Inertia::render('Cloud/ResourceShow', ['resource' => $resource->load('account')]);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Container;
use App\Http\Controllers\Controller;
use App\Models\Container;
use App\Models\DockerEndpoint;
use App\Services\Container\DockerManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class ContainerController extends Controller
{
public function __construct(private DockerManager $docker) {}
public function index()
{
return Inertia::render('Container/Index', [
'containers' => Container::with('endpoint')->orderBy('name')->paginate(50),
'endpoints' => DockerEndpoint::orderBy('name')->get(),
]);
}
public function show(Container $container)
{
return Inertia::render('Container/Show', ['container' => $container->load('endpoint')]);
}
public function action(Request $request, Container $container)
{
$action = $request->validate(['action' => 'required|string'])['action'];
$this->docker->action($container->endpoint, $container->container_id, $action);
return back()->with('success', "Container {$action} executed.");
}
public function logs(Container $container)
{
$logs = $this->docker->logs($container->endpoint, $container->container_id);
return Inertia::render('Container/Logs', ['container' => $container, 'logs' => $logs]);
}
public function stats(Container $container)
{
$stats = $this->docker->stats($container->endpoint, $container->container_id);
return response()->json($stats);
}
public function destroy(Container $container)
{
$this->docker->removeContainer($container->endpoint, $container->container_id, true);
$container->delete();
return redirect()->route('containers.index')->with('success', 'Container removed.');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers\Container;
use App\Http\Controllers\Controller;
use App\Models\DockerEndpoint;
use App\Models\Stack;
use App\Services\Container\DockerManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class StackController extends Controller
{
public function __construct(private DockerManager $docker) {}
public function index()
{
return Inertia::render('Container/Stacks', [
'stacks' => Stack::with('endpoint')->orderBy('name')->paginate(20),
'endpoints' => DockerEndpoint::all(),
]);
}
public function create()
{
return Inertia::render('Container/StackCreate', ['endpoints' => DockerEndpoint::all()]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'endpoint_id' => 'required|exists:docker_endpoints,id',
'compose_content' => 'required|string',
'env_vars' => 'nullable|array',
]);
Stack::create([...$validated, 'created_by' => auth()->id()]);
return redirect()->route('stacks.index')->with('success', 'Stack created.');
}
public function show(Stack $stack)
{
return Inertia::render('Container/StackShow', ['stack' => $stack->load('endpoint')]);
}
public function edit(Stack $stack)
{
return Inertia::render('Container/StackEdit', ['stack' => $stack, 'endpoints' => DockerEndpoint::all()]);
}
public function update(Request $request, Stack $stack)
{
$stack->update($request->only('name', 'compose_content', 'env_vars'));
return redirect()->route('stacks.show', $stack)->with('success', 'Stack updated.');
}
public function destroy(Stack $stack)
{
$stack->delete();
return redirect()->route('stacks.index')->with('success', 'Stack deleted.');
}
public function deploy(Stack $stack)
{
$this->docker->deployStack($stack->endpoint, $stack->name, $stack->compose_content, $stack->env_vars ?? []);
return back()->with('success', 'Stack deployed.');
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace App\Http\Controllers;
use App\Models\Alert;
use App\Models\CloudAccount;
use App\Models\Container;
use App\Models\DockerEndpoint;
use App\Models\Hypervisor;
use App\Models\SecurityScore;
use App\Models\VirtualMachine;
use Inertia\Inertia;
use Inertia\Response;
class DashboardController extends Controller
{
public function index(): Response
{
$hypervisors = Hypervisor::select('id', 'name', 'type', 'status', 'last_sync_at')->get();
$vmStats = VirtualMachine::selectRaw('
COUNT(*) as total,
SUM(CASE WHEN status = "running" THEN 1 ELSE 0 END) as running,
SUM(CASE WHEN status = "stopped" THEN 1 ELSE 0 END) as stopped,
AVG(cpu_usage) as avg_cpu,
AVG(memory_usage) as avg_memory
')->first();
$containerStats = Container::selectRaw('
COUNT(*) as total,
SUM(CASE WHEN state = "running" THEN 1 ELSE 0 END) as running,
SUM(CASE WHEN state = "exited" THEN 1 ELSE 0 END) as stopped
')->first();
$endpoints = DockerEndpoint::select('id', 'name', 'type', 'status')->get();
$cloudAccounts = CloudAccount::select('id', 'name', 'provider', 'status')->get();
$criticalAlerts = Alert::where('status', 'firing')->where('severity', 'critical')->count();
$warningAlerts = Alert::where('status', 'firing')->where('severity', 'warning')->count();
$recentAlerts = Alert::with('rule')
->where('status', 'firing')
->orderByDesc('created_at')
->limit(5)
->get();
$avgSecurityScore = SecurityScore::avg('score') ?? 0;
return Inertia::render('Dashboard/Index', [
'stats' => [
'hypervisors' => $hypervisors->count(),
'hypervisors_online' => $hypervisors->where('status', 'online')->count(),
'vms' => (int) ($vmStats->total ?? 0),
'vms_running' => (int) ($vmStats->running ?? 0),
'vms_stopped' => (int) ($vmStats->stopped ?? 0),
'avg_cpu' => round((float) ($vmStats->avg_cpu ?? 0), 1),
'avg_memory' => round((float) ($vmStats->avg_memory ?? 0), 1),
'containers' => (int) ($containerStats->total ?? 0),
'containers_running' => (int) ($containerStats->running ?? 0),
'cloud_accounts' => $cloudAccounts->count(),
'critical_alerts' => $criticalAlerts,
'warning_alerts' => $warningAlerts,
'security_score' => round($avgSecurityScore, 1),
],
'hypervisors' => $hypervisors,
'endpoints' => $endpoints,
'cloud_accounts' => $cloudAccounts,
'recent_alerts' => $recentAlerts,
]);
}
}

View File

@@ -0,0 +1,121 @@
<?php
namespace App\Http\Controllers\Hypervisor;
use App\Http\Controllers\Controller;
use App\Models\Hypervisor;
use App\Services\Hypervisor\HypervisorManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class HypervisorController extends Controller
{
public function __construct(private HypervisorManager $manager) {}
public function index()
{
$hypervisors = Hypervisor::withCount('virtualMachines')
->orderByDesc('created_at')
->paginate(20);
return Inertia::render('Hypervisor/Index', ['hypervisors' => $hypervisors]);
}
public function create()
{
return Inertia::render('Hypervisor/Create');
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'type' => 'required|in:vcenter,esxi,proxmox,hyper-v,xen,kvm',
'host' => 'required|string',
'port' => 'required|integer|between:1,65535',
'username' => 'required|string',
'password' => 'required|string',
'ssl_verify' => 'boolean',
'datacenter' => 'nullable|string',
]);
$hypervisor = Hypervisor::create([
...$validated,
'created_by' => auth()->id(),
'status' => 'unknown',
]);
activity()->causedBy(auth()->user())->performedOn($hypervisor)->log('Hypervisor created');
return redirect()->route('hypervisors.show', $hypervisor)
->with('success', 'Hypervisor added successfully.');
}
public function show(Hypervisor $hypervisor)
{
$hypervisor->load('virtualMachines');
$vmStats = $hypervisor->virtualMachines()
->selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status');
return Inertia::render('Hypervisor/Show', [
'hypervisor' => $hypervisor,
'vm_stats' => $vmStats,
'vms' => $hypervisor->virtualMachines()->orderBy('name')->paginate(50),
]);
}
public function edit(Hypervisor $hypervisor)
{
return Inertia::render('Hypervisor/Edit', ['hypervisor' => $hypervisor->makeVisible('password')]);
}
public function update(Request $request, Hypervisor $hypervisor)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'host' => 'required|string',
'port' => 'required|integer|between:1,65535',
'username' => 'required|string',
'password' => 'nullable|string',
'ssl_verify' => 'boolean',
'datacenter' => 'nullable|string',
]);
if (empty($validated['password'])) {
unset($validated['password']);
}
$hypervisor->update($validated);
activity()->causedBy(auth()->user())->performedOn($hypervisor)->log('Hypervisor updated');
return redirect()->route('hypervisors.show', $hypervisor)
->with('success', 'Hypervisor updated.');
}
public function destroy(Hypervisor $hypervisor)
{
$hypervisor->delete();
activity()->causedBy(auth()->user())->log("Hypervisor '{$hypervisor->name}' deleted");
return redirect()->route('hypervisors.index')->with('success', 'Hypervisor removed.');
}
public function sync(Hypervisor $hypervisor)
{
try {
$this->manager->driver($hypervisor)->sync();
return back()->with('success', 'Sync completed successfully.');
} catch (\Throwable $e) {
$hypervisor->update(['status' => 'error', 'last_error' => $e->getMessage()]);
return back()->with('error', 'Sync failed: ' . $e->getMessage());
}
}
public function testConnection(Request $request, Hypervisor $hypervisor)
{
$result = $this->manager->testConnection($hypervisor);
return response()->json($result);
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Hypervisor;
use App\Http\Controllers\Controller;
use App\Models\Hypervisor;
use App\Models\VmTemplate;
use Illuminate\Http\Request;
use Inertia\Inertia;
class TemplateController extends Controller
{
public function index()
{
return Inertia::render('Hypervisor/Templates', [
'templates' => VmTemplate::with('hypervisor')->orderBy('name')->paginate(20),
]);
}
public function create()
{
return Inertia::render('Hypervisor/TemplateCreate', [
'hypervisors' => Hypervisor::where('status', 'online')->get(),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string|max:100',
'hypervisor_id' => 'nullable|exists:hypervisors,id',
'os_type' => 'required|string',
'cpu_count' => 'integer|min:1',
'memory_mb' => 'integer|min:512',
'disk_gb' => 'integer|min:1',
'customization' => 'nullable|array',
'provision_script' => 'nullable|string',
]);
VmTemplate::create([...$validated, 'created_by' => auth()->id()]);
return redirect()->route('templates.index')->with('success', 'Template created.');
}
public function show(VmTemplate $template)
{
return Inertia::render('Hypervisor/TemplateShow', ['template' => $template->load('hypervisor')]);
}
public function edit(VmTemplate $template)
{
return Inertia::render('Hypervisor/TemplateEdit', [
'template' => $template,
'hypervisors' => Hypervisor::where('status', 'online')->get(),
]);
}
public function update(Request $request, VmTemplate $template)
{
$template->update($request->except('_token'));
return redirect()->route('templates.show', $template)->with('success', 'Template updated.');
}
public function destroy(VmTemplate $template)
{
$template->delete();
return redirect()->route('templates.index')->with('success', 'Template deleted.');
}
public function deploy(Request $request, VmTemplate $template)
{
$data = $request->validate([
'vm_name' => 'required|string',
'hypervisor_id' => 'required|exists:hypervisors,id',
]);
// Dispatch deploy job
return back()->with('success', 'Deployment queued.');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Hypervisor;
use App\Http\Controllers\Controller;
use App\Models\VirtualMachine;
use App\Services\Hypervisor\HypervisorManager;
use Illuminate\Http\Request;
use Inertia\Inertia;
class VirtualMachineController extends Controller
{
public function __construct(private HypervisorManager $manager) {}
public function index()
{
return Inertia::render('Hypervisor/VMs', [
'vms' => VirtualMachine::with('hypervisor')->orderBy('name')->paginate(50),
]);
}
public function show(VirtualMachine $vm)
{
return Inertia::render('Hypervisor/VMShow', ['vm' => $vm->load('hypervisor')]);
}
public function power(Request $request, VirtualMachine $vm)
{
$action = $request->validate(['action' => 'required|in:start,stop,reboot,suspend'])['action'];
$driver = $this->manager->driver($vm->hypervisor);
match ($action) {
'start' => $driver->powerOn($vm->remote_id),
'stop' => $driver->powerOff($vm->remote_id),
'reboot' => $driver->reboot($vm->remote_id),
'suspend' => $driver->suspend($vm->remote_id),
};
return back()->with('success', "VM {$action} initiated.");
}
public function snapshot(Request $request, VirtualMachine $vm)
{
$data = $request->validate(['name' => 'required|string', 'description' => 'nullable|string']);
$this->manager->driver($vm->hypervisor)->createSnapshot($vm->remote_id, $data['name'], $data['description'] ?? '');
return back()->with('success', 'Snapshot created.');
}
public function clone(Request $request, VirtualMachine $vm)
{
$data = $request->validate(['name' => 'required|string']);
$this->manager->driver($vm->hypervisor)->cloneVM($vm->remote_id, $data['name'], []);
return back()->with('success', 'Clone initiated.');
}
public function migrate(Request $request, VirtualMachine $vm)
{
return back()->with('info', 'Migration not yet implemented for this hypervisor type.');
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Http\Controllers\Identity;
use App\Http\Controllers\Controller;
use App\Models\Certificate;
use Illuminate\Http\Request;
use Inertia\Inertia;
class CertificateController extends Controller
{
public function index()
{
return Inertia::render('Identity/Certificates', [
'certificates' => Certificate::orderBy('expires_at')->paginate(20),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'name' => 'required|string',
'domain' => 'required|string',
'certificate' => 'required|string',
'private_key' => 'required|string',
'type' => 'required|in:self-signed,lets-encrypt,custom',
'expires_at' => 'required|date',
]);
Certificate::create([...$validated, 'status' => 'valid', 'created_by' => auth()->id()]);
return back()->with('success', 'Certificate imported.');
}
public function show(Certificate $cert)
{
return Inertia::render('Identity/CertShow', ['cert' => $cert]);
}
public function destroy(Certificate $cert)
{
$cert->delete();
return back()->with('success', 'Certificate deleted.');
}
public function generate(Request $request)
{
$data = $request->validate(['domain' => 'required|string', 'days' => 'integer|min:1|max:3650']);
$days = $data['days'] ?? 365;
$privateKey = openssl_pkey_new(['private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA]);
$csr = openssl_csr_new(['CN' => $data['domain']], $privateKey);
$cert = openssl_csr_sign($csr, null, $privateKey, $days);
openssl_x509_export($cert, $certOut);
openssl_pkey_export($privateKey, $keyOut);
Certificate::create([
'name' => $data['domain'],
'domain' => $data['domain'],
'certificate' => $certOut,
'private_key' => $keyOut,
'type' => 'self-signed',
'expires_at' => now()->addDays($days),
'status' => 'valid',
'created_by' => auth()->id(),
]);
return back()->with('success', 'Self-signed certificate generated.');
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace App\Http\Controllers\Identity;
use App\Http\Controllers\Controller;
use App\Models\PasswordEntry;
use Illuminate\Http\Request;
use Inertia\Inertia;
class PasswordController extends Controller
{
public function index()
{
return Inertia::render('Identity/Index', [
'passwords' => auth()->user()->passwordEntries()->orderBy('title')->get(),
'certificates' => [],
'ssh_keys' => [],
'stats' => [
'passwords' => auth()->user()->passwordEntries()->count(),
'certs' => 0,
'expiring' => 0,
'ssh_keys' => 0,
],
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'title' => 'required|string',
'username' => 'nullable|string',
'password' => 'required|string',
'url' => 'nullable|url',
'notes' => 'nullable|string',
'category' => 'nullable|string',
]);
auth()->user()->passwordEntries()->create($validated);
return back()->with('success', 'Password entry created.');
}
public function reveal(PasswordEntry $password)
{
$this->authorize('view', $password);
return response()->json(['password' => $password->getDecryptedPassword()]);
}
public function update(Request $request, PasswordEntry $password)
{
$this->authorize('update', $password);
$password->update($request->except('_token'));
return back()->with('success', 'Entry updated.');
}
public function destroy(PasswordEntry $password)
{
$this->authorize('delete', $password);
$password->delete();
return back()->with('success', 'Entry deleted.');
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace App\Http\Controllers\Monitoring;
use App\Http\Controllers\Controller;
use App\Models\Alert;
use App\Models\AlertRule;
use App\Models\Metric;
use Inertia\Inertia;
class MonitoringController extends Controller
{
public function index()
{
return Inertia::render('Monitoring/Index', [
'alerts' => Alert::with('rule')->orderByDesc('created_at')->limit(50)->get(),
'alert_stats' => [
'total_rules' => AlertRule::count(),
'resolved_24h' => Alert::where('status', 'resolved')->where('resolved_at', '>=', now()->subDay())->count(),
],
'dashboards' => [],
]);
}
public function metrics(string $target)
{
[$type, $id] = explode(':', $target, 2);
$metrics = Metric::where('target_type', $type)
->where('target_id', $id)
->where('recorded_at', '>=', now()->subHour())
->orderBy('recorded_at')
->get();
return response()->json($metrics);
}
public function alerts()
{
return Inertia::render('Monitoring/Alerts', [
'rules' => AlertRule::orderByDesc('created_at')->paginate(20),
]);
}
public function acknowledge(Alert $alert)
{
$alert->update([
'status' => 'acknowledged',
'acknowledged_by' => auth()->id(),
'acknowledged_at' => now(),
]);
return back()->with('success', 'Alert acknowledged.');
}
public function dashboards()
{
return Inertia::render('Monitoring/Dashboards');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Http\Controllers\Security;
use App\Http\Controllers\Controller;
use App\Models\SecurityScan;
use Illuminate\Http\Request;
use Inertia\Inertia;
class ScanController extends Controller
{
public function index()
{
return Inertia::render('Security/Scans', [
'scans' => SecurityScan::orderByDesc('created_at')->paginate(20),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'target_type' => 'required|string',
'target_id' => 'required|integer',
'scan_type' => 'required|string',
]);
SecurityScan::create([
...$validated,
'target_name' => 'Scan Target',
'status' => 'queued',
'initiated_by' => auth()->id(),
]);
// Dispatch scan job
// \App\Jobs\RunSecurityScan::dispatch($scan);
return back()->with('success', 'Scan queued.');
}
public function show(SecurityScan $scan)
{
return Inertia::render('Security/ScanShow', ['scan' => $scan]);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Security;
use App\Http\Controllers\Controller;
use App\Models\SecurityScan;
use App\Models\SecurityScore;
use Inertia\Inertia;
class SecurityDashboardController extends Controller
{
public function index()
{
$scans = SecurityScan::orderByDesc('created_at')->paginate(15);
$scores = SecurityScore::selectRaw('AVG(score) as avg, MIN(score) as min, MAX(score) as max')->first();
$stats = SecurityScan::whereIn('status', ['completed'])->selectRaw('
SUM(critical_count) as critical,
SUM(high_count) as high,
SUM(medium_count) as medium,
SUM(low_count) as low
')->first();
return Inertia::render('Security/Index', [
'scans' => $scans,
'scores' => $scores,
'stats' => $stats,
]);
}
public function vulnerabilities()
{
return Inertia::render('Security/Vulnerabilities');
}
public function compliance()
{
return Inertia::render('Security/Compliance');
}
}

View File

@@ -0,0 +1,96 @@
<?php
namespace App\Http\Controllers\Wiki;
use App\Http\Controllers\Controller;
use App\Models\WikiPage;
use App\Models\WikiSpace;
use Illuminate\Http\Request;
use Inertia\Inertia;
use League\CommonMark\CommonMarkConverter;
class DocumentController extends Controller
{
private CommonMarkConverter $markdown;
public function __construct()
{
$this->markdown = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
}
public function create()
{
return Inertia::render('Wiki/PageCreate', [
'spaces' => WikiSpace::all(),
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'space_id' => 'required|exists:wiki_spaces,id',
'title' => 'required|string|max:255',
'content' => 'required|string',
'parent_id' => 'nullable|exists:wiki_pages,id',
'tags' => 'nullable|array',
]);
$slug = \Str::slug($validated['title']);
$page = WikiPage::create([
...$validated,
'slug' => $slug,
'content_html' => $this->markdown->convert($validated['content'])->getContent(),
'created_by' => auth()->id(),
'updated_by' => auth()->id(),
]);
return redirect()->route('wiki.pages.show', $page)->with('success', 'Page created.');
}
public function show(WikiPage $page)
{
return Inertia::render('Wiki/PageShow', [
'page' => $page->load('space', 'createdBy', 'updatedBy'),
'children' => $page->children()->get(),
]);
}
public function edit(WikiPage $page)
{
return Inertia::render('Wiki/PageEdit', [
'page' => $page,
'spaces' => WikiSpace::all(),
]);
}
public function update(Request $request, WikiPage $page)
{
$validated = $request->validate([
'title' => 'required|string|max:255',
'content' => 'required|string',
]);
// Save revision
$page->revisions()->create([
'content' => $page->content,
'created_by' => auth()->id(),
]);
$page->update([
...$validated,
'content_html' => $this->markdown->convert($validated['content'])->getContent(),
'updated_by' => auth()->id(),
]);
return redirect()->route('wiki.pages.show', $page)->with('success', 'Page updated.');
}
public function destroy(WikiPage $page)
{
$page->delete();
return redirect()->route('wiki.index')->with('success', 'Page deleted.');
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Http\Controllers\Wiki;
use App\Http\Controllers\Controller;
use App\Models\WikiPage;
use App\Models\WikiSpace;
use Illuminate\Http\Request;
use Inertia\Inertia;
class WikiController extends Controller
{
public function index()
{
return Inertia::render('Wiki/Index', [
'spaces' => WikiSpace::withCount('pages')->get(),
'recent_pages' => WikiPage::with('space')->orderByDesc('updated_at')->limit(20)->get(),
'files' => [],
]);
}
public function spaces()
{
return Inertia::render('Wiki/Spaces', [
'spaces' => WikiSpace::withCount('pages')->get(),
]);
}
public function space(WikiSpace $space)
{
return Inertia::render('Wiki/Space', [
'space' => $space->load('pages'),
'pages' => $space->pages()->orderBy('order')->get(),
]);
}
public function files()
{
return Inertia::render('Wiki/Files');
}
public function uploadFile(Request $request)
{
$request->validate(['file' => 'required|file|max:102400']);
$path = $request->file('file')->store('wiki-files');
return back()->with('success', 'File uploaded.');
}
public function deleteFile(string $file)
{
\Storage::delete('wiki-files/' . $file);
return back()->with('success', 'File deleted.');
}
}