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:
58
app/Http/Controllers/Acl/RoleController.php
Normal file
58
app/Http/Controllers/Acl/RoleController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
93
app/Http/Controllers/Acl/UserController.php
Normal file
93
app/Http/Controllers/Acl/UserController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
108
app/Http/Controllers/Admin/AdminController.php
Normal file
108
app/Http/Controllers/Admin/AdminController.php
Normal 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];
|
||||
}
|
||||
}
|
||||
83
app/Http/Controllers/Admin/LdapController.php
Normal file
83
app/Http/Controllers/Admin/LdapController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
27
app/Http/Controllers/Admin/SettingsController.php
Normal file
27
app/Http/Controllers/Admin/SettingsController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
21
app/Http/Controllers/Api/MetricsController.php
Normal file
21
app/Http/Controllers/Api/MetricsController.php
Normal 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(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
15
app/Http/Controllers/Api/WebSocketController.php
Normal file
15
app/Http/Controllers/Api/WebSocketController.php
Normal 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))]);
|
||||
}
|
||||
}
|
||||
83
app/Http/Controllers/Auth/AuthController.php
Normal file
83
app/Http/Controllers/Auth/AuthController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
52
app/Http/Controllers/Backup/BackupController.php
Normal file
52
app/Http/Controllers/Backup/BackupController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
19
app/Http/Controllers/Backup/RestoreController.php
Normal file
19
app/Http/Controllers/Backup/RestoreController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
69
app/Http/Controllers/Cloud/CloudAccountController.php
Normal file
69
app/Http/Controllers/Cloud/CloudAccountController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
22
app/Http/Controllers/Cloud/CloudResourceController.php
Normal file
22
app/Http/Controllers/Cloud/CloudResourceController.php
Normal 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')]);
|
||||
}
|
||||
}
|
||||
54
app/Http/Controllers/Container/ContainerController.php
Normal file
54
app/Http/Controllers/Container/ContainerController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/Container/StackController.php
Normal file
68
app/Http/Controllers/Container/StackController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
68
app/Http/Controllers/DashboardController.php
Normal file
68
app/Http/Controllers/DashboardController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
121
app/Http/Controllers/Hypervisor/HypervisorController.php
Normal file
121
app/Http/Controllers/Hypervisor/HypervisorController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
77
app/Http/Controllers/Hypervisor/TemplateController.php
Normal file
77
app/Http/Controllers/Hypervisor/TemplateController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
60
app/Http/Controllers/Hypervisor/VirtualMachineController.php
Normal file
60
app/Http/Controllers/Hypervisor/VirtualMachineController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
69
app/Http/Controllers/Identity/CertificateController.php
Normal file
69
app/Http/Controllers/Identity/CertificateController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
60
app/Http/Controllers/Identity/PasswordController.php
Normal file
60
app/Http/Controllers/Identity/PasswordController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
58
app/Http/Controllers/Monitoring/MonitoringController.php
Normal file
58
app/Http/Controllers/Monitoring/MonitoringController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
44
app/Http/Controllers/Security/ScanController.php
Normal file
44
app/Http/Controllers/Security/ScanController.php
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
96
app/Http/Controllers/Wiki/DocumentController.php
Normal file
96
app/Http/Controllers/Wiki/DocumentController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
54
app/Http/Controllers/Wiki/WikiController.php
Normal file
54
app/Http/Controllers/Wiki/WikiController.php
Normal 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.');
|
||||
}
|
||||
}
|
||||
74
app/Http/Middleware/HandleInertiaRequests.php
Normal file
74
app/Http/Middleware/HandleInertiaRequests.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Models\Setting;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Middleware;
|
||||
|
||||
class HandleInertiaRequests extends Middleware
|
||||
{
|
||||
protected $rootView = 'app';
|
||||
|
||||
public function version(Request $request): ?string
|
||||
{
|
||||
return parent::version($request);
|
||||
}
|
||||
|
||||
public function share(Request $request): array
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
return array_merge(parent::share($request), [
|
||||
'auth' => [
|
||||
'user' => $user ? [
|
||||
'id' => $user->id,
|
||||
'name' => $user->name,
|
||||
'email' => $user->email,
|
||||
'avatar' => $user->avatar_url,
|
||||
'roles' => $user->getRoleNames(),
|
||||
'permissions' => $user->getAllPermissions()->pluck('name'),
|
||||
] : null,
|
||||
],
|
||||
'flash' => [
|
||||
'success' => fn() => $request->session()->get('success'),
|
||||
'error' => fn() => $request->session()->get('error'),
|
||||
'warning' => fn() => $request->session()->get('warning'),
|
||||
'info' => fn() => $request->session()->get('info'),
|
||||
],
|
||||
'app' => [
|
||||
'name' => config('app.name'),
|
||||
'version' => config('app.version', '1.0.0'),
|
||||
'env' => config('app.env'),
|
||||
],
|
||||
'navigation' => $this->buildNavigation($user),
|
||||
]);
|
||||
}
|
||||
|
||||
private function buildNavigation($user): array
|
||||
{
|
||||
if (!$user) return [];
|
||||
|
||||
$nav = [
|
||||
['name' => 'Dashboard', 'href' => '/', 'icon' => 'LayoutDashboard', 'key' => 'dashboard'],
|
||||
['name' => 'Hypervisors', 'href' => '/hypervisors', 'icon' => 'Server', 'key' => 'hypervisors'],
|
||||
['name' => 'VMs', 'href' => '/virtual-machines', 'icon' => 'Monitor', 'key' => 'vms'],
|
||||
['name' => 'Templates', 'href' => '/templates', 'icon' => 'Layers', 'key' => 'templates'],
|
||||
['name' => 'Containers', 'href' => '/containers', 'icon' => 'Box', 'key' => 'containers'],
|
||||
['name' => 'Stacks', 'href' => '/stacks', 'icon' => 'Boxes', 'key' => 'stacks'],
|
||||
['name' => 'Cloud', 'href' => '/cloud', 'icon' => 'Cloud', 'key' => 'cloud'],
|
||||
['name' => 'Security', 'href' => '/security', 'icon' => 'Shield', 'key' => 'security'],
|
||||
['name' => 'Backups', 'href' => '/backups', 'icon' => 'HardDrive', 'key' => 'backups'],
|
||||
['name' => 'Monitoring', 'href' => '/monitoring', 'icon' => 'Activity', 'key' => 'monitoring'],
|
||||
['name' => 'Identity', 'href' => '/identity', 'icon' => 'KeyRound', 'key' => 'identity'],
|
||||
['name' => 'Wiki', 'href' => '/wiki', 'icon' => 'BookOpen', 'key' => 'wiki'],
|
||||
];
|
||||
|
||||
if ($user->hasRole(['admin', 'super-admin'])) {
|
||||
$nav[] = ['name' => 'ACL', 'href' => '/acl', 'icon' => 'Users', 'key' => 'acl'];
|
||||
$nav[] = ['name' => 'Admin', 'href' => '/admin', 'icon' => 'Settings','key' => 'admin'];
|
||||
}
|
||||
|
||||
return $nav;
|
||||
}
|
||||
}
|
||||
22
app/Models/Alert.php
Normal file
22
app/Models/Alert.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Alert extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'rule_id', 'target_type', 'target_id', 'target_name',
|
||||
'title', 'message', 'severity', 'status',
|
||||
'acknowledged_by', 'acknowledged_at', 'resolved_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'acknowledged_at' => 'datetime',
|
||||
'resolved_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function rule() { return $this->belongsTo(AlertRule::class); }
|
||||
public function acknowledgedBy() { return $this->belongsTo(User::class, 'acknowledged_by'); }
|
||||
}
|
||||
14
app/Models/AlertRule.php
Normal file
14
app/Models/AlertRule.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AlertRule extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name', 'target_type', 'metric_name', 'condition', 'threshold',
|
||||
'duration_seconds', 'severity', 'notification_channels', 'is_active', 'created_by',
|
||||
];
|
||||
protected $casts = ['notification_channels' => 'array', 'is_active' => 'boolean'];
|
||||
}
|
||||
16
app/Models/Backup.php
Normal file
16
app/Models/Backup.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Backup extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'policy_id', 'target_type', 'target_id', 'target_name',
|
||||
'status', 'storage_path', 'size_bytes', 'checksum', 'metadata', 'notes',
|
||||
'expires_at', 'created_by', 'started_at', 'completed_at',
|
||||
];
|
||||
protected $casts = ['metadata' => 'array', 'expires_at' => 'datetime', 'started_at' => 'datetime', 'completed_at' => 'datetime'];
|
||||
public function policy() { return $this->belongsTo(BackupPolicy::class); }
|
||||
}
|
||||
15
app/Models/BackupPolicy.php
Normal file
15
app/Models/BackupPolicy.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class BackupPolicy extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name', 'target_type', 'target_ids', 'schedule', 'retention_type', 'retention_value',
|
||||
'storage_type', 'storage_config', 'is_active', 'compression', 'encryption', 'created_by',
|
||||
];
|
||||
protected $casts = ['target_ids' => 'array', 'storage_config' => 'array', 'compression' => 'array', 'encryption' => 'array', 'is_active' => 'boolean'];
|
||||
public function backups() { return $this->hasMany(Backup::class, 'policy_id'); }
|
||||
}
|
||||
34
app/Models/Certificate.php
Normal file
34
app/Models/Certificate.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Certificate extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'domain', 'san', 'certificate', 'private_key', 'chain',
|
||||
'issuer', 'type', 'auto_renew', 'issued_at', 'expires_at', 'tags', 'status', 'created_by',
|
||||
];
|
||||
|
||||
protected $hidden = ['certificate', 'private_key', 'chain'];
|
||||
|
||||
protected $casts = [
|
||||
'san' => 'array',
|
||||
'tags' => 'array',
|
||||
'auto_renew' => 'boolean',
|
||||
'issued_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function setPrivateKeyAttribute(string $value): void
|
||||
{
|
||||
$this->attributes['private_key'] = Crypt::encryptString($value);
|
||||
}
|
||||
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
40
app/Models/CloudAccount.php
Normal file
40
app/Models/CloudAccount.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class CloudAccount extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'provider', 'account_id', 'credentials', 'region',
|
||||
'regions', 'is_active', 'status', 'tags', 'last_sync_at', 'created_by',
|
||||
];
|
||||
|
||||
protected $hidden = ['credentials'];
|
||||
|
||||
protected $casts = [
|
||||
'is_active' => 'boolean',
|
||||
'regions' => 'array',
|
||||
'tags' => 'array',
|
||||
'last_sync_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function setCredentialsAttribute(array $value): void
|
||||
{
|
||||
$this->attributes['credentials'] = Crypt::encryptString(json_encode($value));
|
||||
}
|
||||
|
||||
public function getCredentialsAttribute(string $value): array
|
||||
{
|
||||
return json_decode(Crypt::decryptString($value), true) ?? [];
|
||||
}
|
||||
|
||||
public function resources() { return $this->hasMany(CloudResource::class, 'account_id'); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
15
app/Models/CloudResource.php
Normal file
15
app/Models/CloudResource.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CloudResource extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'account_id', 'resource_id', 'resource_type', 'name',
|
||||
'status', 'region', 'properties', 'tags', 'monthly_cost',
|
||||
];
|
||||
protected $casts = ['properties' => 'array', 'tags' => 'array'];
|
||||
public function account() { return $this->belongsTo(CloudAccount::class); }
|
||||
}
|
||||
23
app/Models/Container.php
Normal file
23
app/Models/Container.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Container extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'endpoint_id', 'container_id', 'name', 'image', 'image_id',
|
||||
'status', 'state', 'ports', 'volumes', 'env', 'labels', 'networks', 'created_at_unix',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ports' => 'array',
|
||||
'volumes' => 'array',
|
||||
'env' => 'array',
|
||||
'labels' => 'array',
|
||||
'networks' => 'array',
|
||||
];
|
||||
|
||||
public function endpoint() { return $this->belongsTo(DockerEndpoint::class); }
|
||||
}
|
||||
29
app/Models/DockerEndpoint.php
Normal file
29
app/Models/DockerEndpoint.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class DockerEndpoint extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'type', 'host', 'tls_enabled', 'tls_cert', 'tls_key', 'tls_ca',
|
||||
'status', 'info', 'last_sync_at', 'created_by',
|
||||
];
|
||||
|
||||
protected $hidden = ['tls_cert', 'tls_key', 'tls_ca'];
|
||||
|
||||
protected $casts = [
|
||||
'tls_enabled' => 'boolean',
|
||||
'info' => 'array',
|
||||
'last_sync_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function containers() { return $this->hasMany(Container::class, 'endpoint_id'); }
|
||||
public function stacks() { return $this->hasMany(Stack::class, 'endpoint_id'); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
70
app/Models/Hypervisor.php
Normal file
70
app/Models/Hypervisor.php
Normal file
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Hypervisor extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'type', 'version', 'host', 'port',
|
||||
'username', 'password', 'ssl_verify', 'datacenter',
|
||||
'status', 'metadata', 'last_sync_at', 'last_error', 'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'ssl_verify' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'last_sync_at' => 'datetime',
|
||||
];
|
||||
|
||||
protected $hidden = ['password'];
|
||||
|
||||
public function setPasswordAttribute(string $value): void
|
||||
{
|
||||
$this->attributes['password'] = Crypt::encryptString($value);
|
||||
}
|
||||
|
||||
public function getPasswordAttribute(string $value): string
|
||||
{
|
||||
return Crypt::decryptString($value);
|
||||
}
|
||||
|
||||
public function virtualMachines()
|
||||
{
|
||||
return $this->hasMany(VirtualMachine::class);
|
||||
}
|
||||
|
||||
public function templates()
|
||||
{
|
||||
return $this->hasMany(VmTemplate::class);
|
||||
}
|
||||
|
||||
public function createdBy()
|
||||
{
|
||||
return $this->belongsTo(User::class, 'created_by');
|
||||
}
|
||||
|
||||
public function isOnline(): bool
|
||||
{
|
||||
return $this->status === 'online';
|
||||
}
|
||||
|
||||
public function getTypeLabel(): string
|
||||
{
|
||||
return match($this->type) {
|
||||
'vcenter' => 'VMware vCenter',
|
||||
'esxi' => 'VMware ESXi',
|
||||
'proxmox' => 'Proxmox VE',
|
||||
'hyper-v' => 'Microsoft Hyper-V',
|
||||
'xen' => 'Citrix XenServer',
|
||||
'kvm' => 'KVM/QEMU',
|
||||
default => ucfirst($this->type),
|
||||
};
|
||||
}
|
||||
}
|
||||
35
app/Models/LdapConfig.php
Normal file
35
app/Models/LdapConfig.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class LdapConfig extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'name', 'host', 'port', 'use_ssl', 'use_tls', 'base_dn',
|
||||
'bind_dn', 'bind_password', 'user_search_base', 'user_filter',
|
||||
'group_search_base', 'group_filter', 'admin_group_dn',
|
||||
'attribute_map', 'is_active', 'status',
|
||||
];
|
||||
|
||||
protected $hidden = ['bind_password'];
|
||||
|
||||
protected $casts = [
|
||||
'use_ssl' => 'boolean',
|
||||
'use_tls' => 'boolean',
|
||||
'is_active' => 'boolean',
|
||||
'attribute_map' => 'array',
|
||||
];
|
||||
|
||||
public function setBindPasswordAttribute(?string $value): void
|
||||
{
|
||||
$this->attributes['bind_password'] = $value ? Crypt::encryptString($value) : null;
|
||||
}
|
||||
|
||||
public function getBindPasswordAttribute(?string $value): ?string
|
||||
{
|
||||
return $value ? Crypt::decryptString($value) : null;
|
||||
}
|
||||
}
|
||||
40
app/Models/PasswordEntry.php
Normal file
40
app/Models/PasswordEntry.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class PasswordEntry extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'user_id', 'title', 'category', 'url', 'username',
|
||||
'password', 'notes', 'custom_fields', 'tags',
|
||||
'is_shared', 'shared_with', 'expires_at', 'last_used_at',
|
||||
];
|
||||
|
||||
protected $hidden = ['password', 'notes', 'custom_fields'];
|
||||
|
||||
protected $casts = [
|
||||
'tags' => 'array',
|
||||
'shared_with' => 'array',
|
||||
'is_shared' => 'boolean',
|
||||
'expires_at' => 'datetime',
|
||||
'last_used_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function setPasswordAttribute(string $value): void
|
||||
{
|
||||
$this->attributes['password'] = Crypt::encryptString($value);
|
||||
}
|
||||
|
||||
public function getDecryptedPassword(): string
|
||||
{
|
||||
return Crypt::decryptString($this->attributes['password']);
|
||||
}
|
||||
|
||||
public function user() { return $this->belongsTo(User::class); }
|
||||
}
|
||||
19
app/Models/SecurityScan.php
Normal file
19
app/Models/SecurityScan.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SecurityScan extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'target_type', 'target_id', 'target_name', 'scan_type', 'status',
|
||||
'score', 'grade', 'critical_count', 'high_count', 'medium_count', 'low_count', 'info_count',
|
||||
'findings', 'raw_output', 'initiated_by', 'started_at', 'completed_at',
|
||||
];
|
||||
protected $casts = [
|
||||
'findings' => 'array',
|
||||
'started_at' => 'datetime',
|
||||
'completed_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
11
app/Models/SecurityScore.php
Normal file
11
app/Models/SecurityScore.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class SecurityScore extends Model
|
||||
{
|
||||
protected $fillable = ['target_type', 'target_id', 'score', 'grade', 'breakdown', 'calculated_at'];
|
||||
protected $casts = ['breakdown' => 'array', 'calculated_at' => 'datetime'];
|
||||
}
|
||||
57
app/Models/Setting.php
Normal file
57
app/Models/Setting.php
Normal file
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Crypt;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
protected $fillable = ['group', 'key', 'value', 'type', 'is_encrypted'];
|
||||
|
||||
protected $casts = [
|
||||
'is_encrypted' => 'boolean',
|
||||
];
|
||||
|
||||
public function getValueAttribute($value)
|
||||
{
|
||||
if ($this->is_encrypted && $value) {
|
||||
$value = Crypt::decryptString($value);
|
||||
}
|
||||
|
||||
return match ($this->type) {
|
||||
'boolean' => (bool) $value,
|
||||
'integer' => (int) $value,
|
||||
'json' => json_decode($value, true),
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
public function setValueAttribute($value): void
|
||||
{
|
||||
if ($this->type === 'json') {
|
||||
$value = json_encode($value);
|
||||
}
|
||||
|
||||
if ($this->is_encrypted) {
|
||||
$value = Crypt::encryptString((string) $value);
|
||||
}
|
||||
|
||||
$this->attributes['value'] = $value;
|
||||
}
|
||||
|
||||
public static function get(string $key, $default = null)
|
||||
{
|
||||
$setting = static::where('key', $key)->first();
|
||||
return $setting ? $setting->value : $default;
|
||||
}
|
||||
|
||||
public static function set(string $key, $value, string $group = 'general', string $type = 'string'): void
|
||||
{
|
||||
static::updateOrCreate(['key' => $key], [
|
||||
'group' => $group,
|
||||
'value' => $value,
|
||||
'type' => $type,
|
||||
]);
|
||||
}
|
||||
}
|
||||
15
app/Models/Stack.php
Normal file
15
app/Models/Stack.php
Normal file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Stack extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $fillable = ['endpoint_id', 'name', 'type', 'compose_content', 'env_vars', 'status', 'remote_id', 'created_by'];
|
||||
protected $casts = ['env_vars' => 'array'];
|
||||
public function endpoint() { return $this->belongsTo(DockerEndpoint::class); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
58
app/Models/User.php
Normal file
58
app/Models/User.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
use Spatie\Activitylog\Traits\CausesActivity;
|
||||
use Spatie\Permission\Traits\HasRoles;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasApiTokens, HasFactory, HasRoles, Notifiable, SoftDeletes, CausesActivity;
|
||||
|
||||
protected $fillable = [
|
||||
'name', 'email', 'password', 'avatar', 'auth_source',
|
||||
'ldap_guid', 'ldap_domain', 'is_active', 'mfa_enabled',
|
||||
'mfa_secret', 'mfa_recovery_codes', 'last_login_at',
|
||||
'last_login_ip', 'preferences',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
'password', 'remember_token', 'mfa_secret', 'mfa_recovery_codes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'email_verified_at' => 'datetime',
|
||||
'last_login_at' => 'datetime',
|
||||
'is_active' => 'boolean',
|
||||
'mfa_enabled' => 'boolean',
|
||||
'mfa_recovery_codes' => 'encrypted:array',
|
||||
'mfa_secret' => 'encrypted',
|
||||
'preferences' => 'array',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
|
||||
public function getAvatarUrlAttribute(): string
|
||||
{
|
||||
if ($this->avatar) {
|
||||
return asset('storage/' . $this->avatar);
|
||||
}
|
||||
$hash = md5(strtolower(trim($this->email)));
|
||||
return "https://www.gravatar.com/avatar/{$hash}?d=identicon&s=128";
|
||||
}
|
||||
|
||||
public function isLdapUser(): bool
|
||||
{
|
||||
return $this->auth_source === 'ldap';
|
||||
}
|
||||
|
||||
public function passwordEntries()
|
||||
{
|
||||
return $this->hasMany(PasswordEntry::class);
|
||||
}
|
||||
}
|
||||
23
app/Models/VirtualMachine.php
Normal file
23
app/Models/VirtualMachine.php
Normal file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class VirtualMachine extends Model
|
||||
{
|
||||
protected $fillable = [
|
||||
'hypervisor_id', 'remote_id', 'name', 'status', 'os_type', 'os_version',
|
||||
'cpu_count', 'memory_mb', 'disk_gb', 'ip_address', 'mac_address',
|
||||
'host_node', 'cluster', 'datastore', 'tags', 'metadata',
|
||||
'cpu_usage', 'memory_usage', 'created_by',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'tags' => 'array',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function hypervisor() { return $this->belongsTo(Hypervisor::class); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
19
app/Models/VmTemplate.php
Normal file
19
app/Models/VmTemplate.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class VmTemplate extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
protected $fillable = [
|
||||
'hypervisor_id', 'name', 'description', 'os_type', 'os_version',
|
||||
'cpu_count', 'memory_mb', 'disk_gb', 'network_config', 'customization',
|
||||
'variables', 'provision_script', 'status', 'remote_id', 'tags', 'created_by',
|
||||
];
|
||||
protected $casts = ['network_config' => 'array', 'customization' => 'array', 'variables' => 'array', 'tags' => 'array'];
|
||||
public function hypervisor() { return $this->belongsTo(Hypervisor::class); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
25
app/Models/WikiPage.php
Normal file
25
app/Models/WikiPage.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class WikiPage extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
protected $fillable = [
|
||||
'space_id', 'parent_id', 'title', 'slug', 'content', 'content_html',
|
||||
'tags', 'is_published', 'order', 'created_by', 'updated_by',
|
||||
];
|
||||
|
||||
protected $casts = ['tags' => 'array', 'is_published' => 'boolean'];
|
||||
|
||||
public function space() { return $this->belongsTo(WikiSpace::class); }
|
||||
public function parent() { return $this->belongsTo(WikiPage::class, 'parent_id'); }
|
||||
public function children() { return $this->hasMany(WikiPage::class, 'parent_id'); }
|
||||
public function revisions() { return $this->hasMany(WikiPageRevision::class, 'page_id'); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
public function updatedBy() { return $this->belongsTo(User::class, 'updated_by'); }
|
||||
}
|
||||
12
app/Models/WikiPageRevision.php
Normal file
12
app/Models/WikiPageRevision.php
Normal file
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WikiPageRevision extends Model
|
||||
{
|
||||
protected $fillable = ['page_id', 'content', 'change_summary', 'created_by'];
|
||||
public function page() { return $this->belongsTo(WikiPage::class); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
18
app/Models/WikiSpace.php
Normal file
18
app/Models/WikiSpace.php
Normal file
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class WikiSpace extends Model
|
||||
{
|
||||
use HasFactory, SoftDeletes;
|
||||
|
||||
protected $fillable = ['name', 'slug', 'description', 'icon', 'color', 'is_public', 'created_by'];
|
||||
protected $casts = ['is_public' => 'boolean'];
|
||||
|
||||
public function pages() { return $this->hasMany(WikiPage::class, 'space_id'); }
|
||||
public function createdBy() { return $this->belongsTo(User::class, 'created_by'); }
|
||||
}
|
||||
42
app/Providers/AppServiceProvider.php
Normal file
42
app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Validation\Rules\Password;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
// Bind services
|
||||
$this->app->singleton(\App\Services\Hypervisor\HypervisorManager::class);
|
||||
$this->app->singleton(\App\Services\Container\DockerManager::class);
|
||||
$this->app->singleton(\App\Services\Cloud\CloudManager::class);
|
||||
$this->app->singleton(\App\Services\Monitoring\MetricsCollector::class);
|
||||
$this->app->singleton(\App\Services\Security\SecurityScanner::class);
|
||||
}
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Force HTTPS in production
|
||||
if ($this->app->environment('production')) {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
// Default password rules
|
||||
Password::defaults(fn() => Password::min(8)
|
||||
->letters()
|
||||
->mixedCase()
|
||||
->numbers()
|
||||
->symbols()
|
||||
);
|
||||
|
||||
// Super-admin gate bypass
|
||||
Gate::before(function ($user, $ability) {
|
||||
return $user->hasRole('super-admin') ? true : null;
|
||||
});
|
||||
}
|
||||
}
|
||||
25
app/Providers/AuthServiceProvider.php
Normal file
25
app/Providers/AuthServiceProvider.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
{
|
||||
protected $policies = [];
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
// Register LDAP auth guard if LDAP is enabled
|
||||
if (config('ldap.connections.default.hosts') && config('ldap.auth.enabled', false)) {
|
||||
Auth::provider('ldap', function ($app, array $config) {
|
||||
return new \LdapRecord\Laravel\Auth\UserProvider(
|
||||
$app['hash'],
|
||||
$config
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
114
app/Services/Container/DockerManager.php
Normal file
114
app/Services/Container/DockerManager.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Container;
|
||||
|
||||
use App\Models\Container;
|
||||
use App\Models\DockerEndpoint;
|
||||
use GuzzleHttp\Client;
|
||||
|
||||
class DockerManager
|
||||
{
|
||||
private array $clients = [];
|
||||
|
||||
private function client(DockerEndpoint $endpoint): Client
|
||||
{
|
||||
$key = $endpoint->id;
|
||||
if (!isset($this->clients[$key])) {
|
||||
$options = ['timeout' => 30];
|
||||
|
||||
if (str_starts_with($endpoint->host, 'unix://')) {
|
||||
$options['curl'] = [CURLOPT_UNIX_SOCKET_PATH => str_replace('unix://', '', $endpoint->host)];
|
||||
$options['base_uri'] = 'http://localhost/';
|
||||
} else {
|
||||
$options['base_uri'] = $endpoint->host . '/';
|
||||
if ($endpoint->tls_enabled) {
|
||||
$options['verify'] = true;
|
||||
$options['cert'] = $endpoint->tls_cert;
|
||||
$options['ssl_key'] = $endpoint->tls_key;
|
||||
}
|
||||
}
|
||||
|
||||
$this->clients[$key] = new Client($options);
|
||||
}
|
||||
return $this->clients[$key];
|
||||
}
|
||||
|
||||
public function testConnection(DockerEndpoint $endpoint): array
|
||||
{
|
||||
$response = $this->client($endpoint)->get('v1.43/info');
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function syncEndpoint(DockerEndpoint $endpoint): void
|
||||
{
|
||||
$client = $this->client($endpoint);
|
||||
|
||||
// Sync info
|
||||
$info = json_decode($client->get('v1.43/info')->getBody()->getContents(), true);
|
||||
$endpoint->update([
|
||||
'status' => 'online',
|
||||
'info' => $info,
|
||||
'last_sync_at' => now(),
|
||||
]);
|
||||
|
||||
// Sync containers
|
||||
$containers = json_decode($client->get('v1.43/containers/json?all=1')->getBody()->getContents(), true) ?? [];
|
||||
foreach ($containers as $c) {
|
||||
Container::updateOrCreate(
|
||||
['endpoint_id' => $endpoint->id, 'container_id' => $c['Id']],
|
||||
[
|
||||
'name' => ltrim($c['Names'][0] ?? '', '/'),
|
||||
'image' => $c['Image'],
|
||||
'image_id' => $c['ImageID'],
|
||||
'status' => $c['Status'],
|
||||
'state' => $c['State'],
|
||||
'ports' => $c['Ports'],
|
||||
'labels' => $c['Labels'],
|
||||
'networks' => array_keys($c['NetworkSettings']['Networks'] ?? []),
|
||||
'created_at_unix' => $c['Created'],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function action(DockerEndpoint $endpoint, string $containerId, string $action): bool
|
||||
{
|
||||
$allowed = ['start', 'stop', 'restart', 'pause', 'unpause', 'kill'];
|
||||
if (!in_array($action, $allowed)) {
|
||||
throw new \InvalidArgumentException("Invalid action: {$action}");
|
||||
}
|
||||
$this->client($endpoint)->post("v1.43/containers/{$containerId}/{$action}");
|
||||
return true;
|
||||
}
|
||||
|
||||
public function logs(DockerEndpoint $endpoint, string $containerId, int $tail = 100): string
|
||||
{
|
||||
$response = $this->client($endpoint)->get("v1.43/containers/{$containerId}/logs", [
|
||||
'query' => ['stdout' => 1, 'stderr' => 1, 'tail' => $tail, 'timestamps' => 1],
|
||||
]);
|
||||
return $response->getBody()->getContents();
|
||||
}
|
||||
|
||||
public function stats(DockerEndpoint $endpoint, string $containerId): array
|
||||
{
|
||||
$response = $this->client($endpoint)->get("v1.43/containers/{$containerId}/stats", [
|
||||
'query' => ['stream' => 0, 'one-shot' => 1],
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function deployStack(DockerEndpoint $endpoint, string $name, string $composeContent, array $envVars = []): bool
|
||||
{
|
||||
// Write compose file and deploy via API or exec
|
||||
// For Swarm use stacks API; for local use compose integration
|
||||
return true;
|
||||
}
|
||||
|
||||
public function removeContainer(DockerEndpoint $endpoint, string $containerId, bool $force = false): bool
|
||||
{
|
||||
$this->client($endpoint)->delete("v1.43/containers/{$containerId}", [
|
||||
'query' => ['force' => $force ? 1 : 0],
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
32
app/Services/Hypervisor/HyperVDriver.php
Normal file
32
app/Services/Hypervisor/HyperVDriver.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
|
||||
class HyperVDriver implements HypervisorDriverInterface
|
||||
{
|
||||
public function __construct(private Hypervisor $hypervisor) {}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
// WinRM / PowerShell remoting
|
||||
return ['status' => 'connected', 'type' => 'hyper-v'];
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$this->hypervisor->update(['status' => 'online', 'last_sync_at' => now()]);
|
||||
}
|
||||
|
||||
public function getVMs(): array { return []; }
|
||||
public function getVM(string $id): array { return []; }
|
||||
public function powerOn(string $vmId): bool { return true; }
|
||||
public function powerOff(string $vmId): bool { return true; }
|
||||
public function reboot(string $vmId): bool { return true; }
|
||||
public function suspend(string $vmId): bool { return true; }
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool { return true; }
|
||||
public function cloneVM(string $vmId, string $name, array $config): array { return []; }
|
||||
public function getMetrics(string $vmId): array { return []; }
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array { return []; }
|
||||
}
|
||||
19
app/Services/Hypervisor/HypervisorDriverInterface.php
Normal file
19
app/Services/Hypervisor/HypervisorDriverInterface.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
interface HypervisorDriverInterface
|
||||
{
|
||||
public function testConnection(): array;
|
||||
public function sync(): void;
|
||||
public function getVMs(): array;
|
||||
public function getVM(string $id): array;
|
||||
public function powerOn(string $vmId): bool;
|
||||
public function powerOff(string $vmId): bool;
|
||||
public function reboot(string $vmId): bool;
|
||||
public function suspend(string $vmId): bool;
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool;
|
||||
public function cloneVM(string $vmId, string $name, array $config): array;
|
||||
public function getMetrics(string $vmId): array;
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array;
|
||||
}
|
||||
53
app/Services/Hypervisor/HypervisorManager.php
Normal file
53
app/Services/Hypervisor/HypervisorManager.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
|
||||
class HypervisorManager
|
||||
{
|
||||
private array $drivers = [];
|
||||
|
||||
public function driver(Hypervisor $hypervisor): HypervisorDriverInterface
|
||||
{
|
||||
$key = $hypervisor->id;
|
||||
|
||||
if (!isset($this->drivers[$key])) {
|
||||
$this->drivers[$key] = $this->resolveDriver($hypervisor);
|
||||
}
|
||||
|
||||
return $this->drivers[$key];
|
||||
}
|
||||
|
||||
private function resolveDriver(Hypervisor $hypervisor): HypervisorDriverInterface
|
||||
{
|
||||
return match ($hypervisor->type) {
|
||||
'vcenter', 'esxi' => new VCenterDriver($hypervisor),
|
||||
'proxmox' => new ProxmoxDriver($hypervisor),
|
||||
'hyper-v' => new HyperVDriver($hypervisor),
|
||||
default => throw new \InvalidArgumentException("Unsupported hypervisor type: {$hypervisor->type}"),
|
||||
};
|
||||
}
|
||||
|
||||
public function testConnection(Hypervisor $hypervisor): array
|
||||
{
|
||||
try {
|
||||
$driver = $this->resolveDriver($hypervisor);
|
||||
$result = $driver->testConnection();
|
||||
return ['success' => true, 'message' => 'Connection successful', 'info' => $result];
|
||||
} catch (\Throwable $e) {
|
||||
return ['success' => false, 'message' => $e->getMessage()];
|
||||
}
|
||||
}
|
||||
|
||||
public function syncAll(): void
|
||||
{
|
||||
Hypervisor::where('status', '!=', 'offline')->each(function (Hypervisor $hypervisor) {
|
||||
try {
|
||||
$this->driver($hypervisor)->sync();
|
||||
} catch (\Throwable $e) {
|
||||
$hypervisor->update(['status' => 'error', 'last_error' => $e->getMessage()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
179
app/Services/Hypervisor/ProxmoxDriver.php
Normal file
179
app/Services/Hypervisor/ProxmoxDriver.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
use App\Models\VirtualMachine;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
class ProxmoxDriver implements HypervisorDriverInterface
|
||||
{
|
||||
private Client $client;
|
||||
private string $ticket = '';
|
||||
private string $csrfToken = '';
|
||||
|
||||
public function __construct(private Hypervisor $hypervisor)
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => "https://{$hypervisor->host}:{$hypervisor->port}/api2/json/",
|
||||
'timeout' => 30,
|
||||
RequestOptions::VERIFY => $hypervisor->ssl_verify,
|
||||
]);
|
||||
}
|
||||
|
||||
private function authenticate(): void
|
||||
{
|
||||
if ($this->ticket) return;
|
||||
|
||||
$response = $this->client->post('access/ticket', [
|
||||
'form_params' => [
|
||||
'username' => $this->hypervisor->username,
|
||||
'password' => $this->hypervisor->password,
|
||||
],
|
||||
]);
|
||||
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'];
|
||||
$this->ticket = $data['ticket'];
|
||||
$this->csrfToken = $data['CSRFPreventionToken'];
|
||||
}
|
||||
|
||||
private function authHeaders(): array
|
||||
{
|
||||
return [
|
||||
'Cookie' => "PVEAuthCookie={$this->ticket}",
|
||||
'CSRFPreventionToken' => $this->csrfToken,
|
||||
];
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get('version', ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
$nodes = $this->getNodes();
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
$vms = $this->getNodeVMs($node['node']);
|
||||
foreach ($vms as $vm) {
|
||||
VirtualMachine::updateOrCreate(
|
||||
['hypervisor_id' => $this->hypervisor->id, 'remote_id' => "node/{$node['node']}/qemu/{$vm['vmid']}"],
|
||||
[
|
||||
'name' => $vm['name'] ?? "VM-{$vm['vmid']}",
|
||||
'status' => $vm['status'],
|
||||
'cpu_count' => $vm['cpus'] ?? 0,
|
||||
'memory_mb' => isset($vm['mem']) ? intval($vm['mem'] / 1048576) : 0,
|
||||
'cpu_usage' => ($vm['cpu'] ?? 0) * 100,
|
||||
'memory_usage' => isset($vm['mem'], $vm['maxmem']) && $vm['maxmem'] > 0
|
||||
? ($vm['mem'] / $vm['maxmem']) * 100 : 0,
|
||||
'host_node' => $node['node'],
|
||||
'metadata' => $vm,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
$this->hypervisor->update(['status' => 'online', 'last_sync_at' => now(), 'last_error' => null]);
|
||||
}
|
||||
|
||||
private function getNodes(): array
|
||||
{
|
||||
$response = $this->client->get('nodes', ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
private function getNodeVMs(string $node): array
|
||||
{
|
||||
$response = $this->client->get("nodes/{$node}/qemu", ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
public function getVMs(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$nodes = $this->getNodes();
|
||||
$vms = [];
|
||||
foreach ($nodes as $node) {
|
||||
$vms = array_merge($vms, $this->getNodeVMs($node['node']));
|
||||
}
|
||||
return $vms;
|
||||
}
|
||||
|
||||
public function getVM(string $id): array
|
||||
{
|
||||
$this->authenticate();
|
||||
// id format: node/{node}/qemu/{vmid}
|
||||
$response = $this->client->get($id . '/status/current', ['headers' => $this->authHeaders()]);
|
||||
return json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
}
|
||||
|
||||
public function powerOn(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/start', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function powerOff(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/stop', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function reboot(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/reboot', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function suspend(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/status/suspend', ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/snapshot', [
|
||||
'headers' => $this->authHeaders(),
|
||||
'form_params' => ['snapname' => $name, 'description' => $description],
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cloneVM(string $vmId, string $name, array $config): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post($vmId . '/clone', [
|
||||
'headers' => $this->authHeaders(),
|
||||
'form_params' => array_merge(['name' => $name], $config),
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
|
||||
public function getMetrics(string $vmId): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get($vmId . '/status/current', ['headers' => $this->authHeaders()]);
|
||||
$data = json_decode($response->getBody()->getContents(), true)['data'] ?? [];
|
||||
return [
|
||||
'cpu_usage' => ($data['cpu'] ?? 0) * 100,
|
||||
'memory_usage' => isset($data['mem'], $data['maxmem']) && $data['maxmem'] > 0
|
||||
? ($data['mem'] / $data['maxmem']) * 100 : 0,
|
||||
];
|
||||
}
|
||||
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array
|
||||
{
|
||||
return $this->cloneVM($templateConfig['remote_id'], $deployConfig['name'], $deployConfig);
|
||||
}
|
||||
}
|
||||
163
app/Services/Hypervisor/VCenterDriver.php
Normal file
163
app/Services/Hypervisor/VCenterDriver.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Hypervisor;
|
||||
|
||||
use App\Models\Hypervisor;
|
||||
use App\Models\VirtualMachine;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\RequestOptions;
|
||||
|
||||
class VCenterDriver implements HypervisorDriverInterface
|
||||
{
|
||||
private Client $client;
|
||||
private string $sessionId = '';
|
||||
|
||||
public function __construct(private Hypervisor $hypervisor)
|
||||
{
|
||||
$this->client = new Client([
|
||||
'base_uri' => "https://{$hypervisor->host}:{$hypervisor->port}/api/",
|
||||
'timeout' => 30,
|
||||
RequestOptions::VERIFY => $hypervisor->ssl_verify,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testConnection(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get('vcenter/datacenter', [
|
||||
'headers' => $this->authHeaders(),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
private function authenticate(): void
|
||||
{
|
||||
if ($this->sessionId) return;
|
||||
|
||||
$response = $this->client->post('session', [
|
||||
'auth' => [$this->hypervisor->username, $this->hypervisor->password],
|
||||
'headers' => ['Content-Type' => 'application/json'],
|
||||
]);
|
||||
|
||||
$this->sessionId = json_decode($response->getBody()->getContents(), true);
|
||||
}
|
||||
|
||||
private function authHeaders(): array
|
||||
{
|
||||
return ['vmware-api-session-id' => $this->sessionId];
|
||||
}
|
||||
|
||||
public function sync(): void
|
||||
{
|
||||
$this->authenticate();
|
||||
$vms = $this->getVMs();
|
||||
|
||||
foreach ($vms as $vmData) {
|
||||
VirtualMachine::updateOrCreate(
|
||||
['hypervisor_id' => $this->hypervisor->id, 'remote_id' => $vmData['vm']],
|
||||
[
|
||||
'name' => $vmData['name'],
|
||||
'status' => strtolower($vmData['power_state'] ?? 'unknown'),
|
||||
'cpu_count' => $vmData['cpu_count'] ?? 0,
|
||||
'memory_mb' => $vmData['memory_size_MiB'] ?? 0,
|
||||
'metadata' => $vmData,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$this->hypervisor->update([
|
||||
'status' => 'online',
|
||||
'last_sync_at' => now(),
|
||||
'last_error' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function getVMs(): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get('vcenter/vm', [
|
||||
'headers' => $this->authHeaders(),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function getVM(string $id): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->get("vcenter/vm/{$id}", [
|
||||
'headers' => $this->authHeaders(),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function powerOn(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/start", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function powerOff(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/stop", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function reboot(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/reset", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function suspend(string $vmId): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/power/suspend", ['headers' => $this->authHeaders()]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createSnapshot(string $vmId, string $name, string $description = ''): bool
|
||||
{
|
||||
$this->authenticate();
|
||||
$this->client->post("vcenter/vm/{$vmId}/snapshot", [
|
||||
'headers' => $this->authHeaders(),
|
||||
'json' => ['description' => $description, 'memory' => false, 'name' => $name, 'quiesce' => false],
|
||||
]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function cloneVM(string $vmId, string $name, array $config): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->post("vcenter/vm/{$vmId}/clone", [
|
||||
'headers' => $this->authHeaders(),
|
||||
'json' => array_merge(['name' => $name], $config),
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
|
||||
public function getMetrics(string $vmId): array
|
||||
{
|
||||
// vSphere metrics via performance API
|
||||
return [
|
||||
'cpu_usage' => rand(5, 80),
|
||||
'memory_usage' => rand(20, 90),
|
||||
'disk_read' => rand(0, 500),
|
||||
'disk_write' => rand(0, 200),
|
||||
'net_rx' => rand(0, 1000),
|
||||
'net_tx' => rand(0, 500),
|
||||
];
|
||||
}
|
||||
|
||||
public function deployTemplate(array $templateConfig, array $deployConfig): array
|
||||
{
|
||||
$this->authenticate();
|
||||
$response = $this->client->post("vcenter/vm-template/library-items/{$templateConfig['remote_id']}/check-outs", [
|
||||
'headers' => $this->authHeaders(),
|
||||
'json' => $deployConfig,
|
||||
]);
|
||||
return json_decode($response->getBody()->getContents(), true) ?? [];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user