From c649f055d831cdbaf1bf92d789fdd2feb3baf059 Mon Sep 17 00:00:00 2001 From: I-Fun Date: Tue, 7 Apr 2026 20:29:32 +0700 Subject: [PATCH] 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. --- .env.example | 69 +++++ .gitignore | 20 ++ app/Http/Controllers/Acl/RoleController.php | 58 ++++ app/Http/Controllers/Acl/UserController.php | 93 +++++++ .../Controllers/Admin/AdminController.php | 108 ++++++++ app/Http/Controllers/Admin/LdapController.php | 83 ++++++ .../Controllers/Admin/SettingsController.php | 27 ++ .../Controllers/Api/MetricsController.php | 21 ++ .../Controllers/Api/WebSocketController.php | 15 ++ app/Http/Controllers/Auth/AuthController.php | 83 ++++++ .../Controllers/Backup/BackupController.php | 52 ++++ .../Controllers/Backup/RestoreController.php | 19 ++ .../Cloud/CloudAccountController.php | 69 +++++ .../Cloud/CloudResourceController.php | 22 ++ .../Container/ContainerController.php | 54 ++++ .../Controllers/Container/StackController.php | 68 +++++ app/Http/Controllers/DashboardController.php | 68 +++++ .../Hypervisor/HypervisorController.php | 121 +++++++++ .../Hypervisor/TemplateController.php | 77 ++++++ .../Hypervisor/VirtualMachineController.php | 60 +++++ .../Identity/CertificateController.php | 69 +++++ .../Identity/PasswordController.php | 60 +++++ .../Monitoring/MonitoringController.php | 58 ++++ .../Controllers/Security/ScanController.php | 44 +++ .../Security/SecurityDashboardController.php | 39 +++ .../Controllers/Wiki/DocumentController.php | 96 +++++++ app/Http/Controllers/Wiki/WikiController.php | 54 ++++ app/Http/Middleware/HandleInertiaRequests.php | 74 +++++ app/Models/Alert.php | 22 ++ app/Models/AlertRule.php | 14 + app/Models/Backup.php | 16 ++ app/Models/BackupPolicy.php | 15 ++ app/Models/Certificate.php | 34 +++ app/Models/CloudAccount.php | 40 +++ app/Models/CloudResource.php | 15 ++ app/Models/Container.php | 23 ++ app/Models/DockerEndpoint.php | 29 ++ app/Models/Hypervisor.php | 70 +++++ app/Models/LdapConfig.php | 35 +++ app/Models/PasswordEntry.php | 40 +++ app/Models/SecurityScan.php | 19 ++ app/Models/SecurityScore.php | 11 + app/Models/Setting.php | 57 ++++ app/Models/Stack.php | 15 ++ app/Models/User.php | 58 ++++ app/Models/VirtualMachine.php | 23 ++ app/Models/VmTemplate.php | 19 ++ app/Models/WikiPage.php | 25 ++ app/Models/WikiPageRevision.php | 12 + app/Models/WikiSpace.php | 18 ++ app/Providers/AppServiceProvider.php | 42 +++ app/Providers/AuthServiceProvider.php | 25 ++ app/Services/Container/DockerManager.php | 114 ++++++++ app/Services/Hypervisor/HyperVDriver.php | 32 +++ .../Hypervisor/HypervisorDriverInterface.php | 19 ++ app/Services/Hypervisor/HypervisorManager.php | 53 ++++ app/Services/Hypervisor/ProxmoxDriver.php | 179 +++++++++++++ app/Services/Hypervisor/VCenterDriver.php | 163 +++++++++++ bootstrap/app.php | 41 +++ bootstrap/providers.php | 8 + composer.json | 77 ++++++ config/activitylog.php | 12 + config/app.php | 45 ++++ config/auth.php | 20 ++ config/cache.php | 11 + config/database.php | 47 ++++ config/ldap.php | 36 +++ config/permission.php | 33 +++ config/queue.php | 12 + config/session.php | 20 ++ .../0001_01_01_000000_create_users_table.php | 55 ++++ .../0001_01_01_000001_create_cache_table.php | 29 ++ .../0001_01_01_000002_create_jobs_table.php | 51 ++++ ...024_01_01_000001_create_settings_table.php | 26 ++ ..._01_01_000002_create_hypervisors_table.php | 102 +++++++ ...4_01_01_000003_create_containers_table.php | 80 ++++++ .../2024_01_01_000004_create_cloud_table.php | 50 ++++ ...024_01_01_000005_create_security_table.php | 64 +++++ ...2024_01_01_000006_create_backups_table.php | 69 +++++ ...4_01_01_000007_create_monitoring_table.php | 75 ++++++ ...024_01_01_000008_create_identity_table.php | 72 +++++ .../2024_01_01_000009_create_wiki_table.php | 73 +++++ .../2024_01_01_000010_create_ldap_table.php | 37 +++ database/seeders/DatabaseSeeder.php | 96 +++++++ docker-compose.yml | 119 ++++++++ docker/nginx/default.conf | 97 +++++++ docker/php/Dockerfile | 92 +++++++ docker/php/opcache.ini | 8 + docker/php/php.ini | 7 + docker/supervisor/supervisord.conf | 32 +++ package.json | 50 ++++ resources/css/app.css | 112 ++++++++ resources/js/Components/ui/Button.jsx | 42 +++ resources/js/Components/ui/DataTable.jsx | 137 ++++++++++ resources/js/Components/ui/GlassCard.jsx | 59 ++++ resources/js/Components/ui/Modal.jsx | 54 ++++ resources/js/Components/ui/ProgressBar.jsx | 40 +++ resources/js/Components/ui/StatusBadge.jsx | 34 +++ resources/js/Layouts/AppLayout.jsx | 253 ++++++++++++++++++ resources/js/Pages/Acl/Index.jsx | 120 +++++++++ resources/js/Pages/Admin/Index.jsx | 93 +++++++ resources/js/Pages/Admin/Ldap.jsx | 146 ++++++++++ resources/js/Pages/Admin/Settings.jsx | 115 ++++++++ resources/js/Pages/Auth/Login.jsx | 154 +++++++++++ resources/js/Pages/Backup/Index.jsx | 114 ++++++++ resources/js/Pages/Cloud/Index.jsx | 150 +++++++++++ resources/js/Pages/Container/Index.jsx | 131 +++++++++ resources/js/Pages/Dashboard/Index.jsx | 238 ++++++++++++++++ resources/js/Pages/Error.jsx | 39 +++ resources/js/Pages/Hypervisor/Create.jsx | 163 +++++++++++ resources/js/Pages/Hypervisor/Index.jsx | 114 ++++++++ resources/js/Pages/Identity/Index.jsx | 161 +++++++++++ resources/js/Pages/Monitoring/Index.jsx | 131 +++++++++ resources/js/Pages/Security/Index.jsx | 138 ++++++++++ resources/js/Pages/Wiki/Index.jsx | 113 ++++++++ resources/js/app.jsx | 24 ++ resources/js/bootstrap.js | 4 + resources/js/lib/utils.js | 71 +++++ resources/views/app.blade.php | 24 ++ routes/api.php | 10 + routes/console.php | 16 ++ routes/web.php | 205 ++++++++++++++ setup.ps1 | 73 +++++ setup.sh | 102 +++++++ tailwind.config.js | 149 +++++++++++ vite.config.js | 29 ++ 126 files changed, 8122 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 app/Http/Controllers/Acl/RoleController.php create mode 100644 app/Http/Controllers/Acl/UserController.php create mode 100644 app/Http/Controllers/Admin/AdminController.php create mode 100644 app/Http/Controllers/Admin/LdapController.php create mode 100644 app/Http/Controllers/Admin/SettingsController.php create mode 100644 app/Http/Controllers/Api/MetricsController.php create mode 100644 app/Http/Controllers/Api/WebSocketController.php create mode 100644 app/Http/Controllers/Auth/AuthController.php create mode 100644 app/Http/Controllers/Backup/BackupController.php create mode 100644 app/Http/Controllers/Backup/RestoreController.php create mode 100644 app/Http/Controllers/Cloud/CloudAccountController.php create mode 100644 app/Http/Controllers/Cloud/CloudResourceController.php create mode 100644 app/Http/Controllers/Container/ContainerController.php create mode 100644 app/Http/Controllers/Container/StackController.php create mode 100644 app/Http/Controllers/DashboardController.php create mode 100644 app/Http/Controllers/Hypervisor/HypervisorController.php create mode 100644 app/Http/Controllers/Hypervisor/TemplateController.php create mode 100644 app/Http/Controllers/Hypervisor/VirtualMachineController.php create mode 100644 app/Http/Controllers/Identity/CertificateController.php create mode 100644 app/Http/Controllers/Identity/PasswordController.php create mode 100644 app/Http/Controllers/Monitoring/MonitoringController.php create mode 100644 app/Http/Controllers/Security/ScanController.php create mode 100644 app/Http/Controllers/Security/SecurityDashboardController.php create mode 100644 app/Http/Controllers/Wiki/DocumentController.php create mode 100644 app/Http/Controllers/Wiki/WikiController.php create mode 100644 app/Http/Middleware/HandleInertiaRequests.php create mode 100644 app/Models/Alert.php create mode 100644 app/Models/AlertRule.php create mode 100644 app/Models/Backup.php create mode 100644 app/Models/BackupPolicy.php create mode 100644 app/Models/Certificate.php create mode 100644 app/Models/CloudAccount.php create mode 100644 app/Models/CloudResource.php create mode 100644 app/Models/Container.php create mode 100644 app/Models/DockerEndpoint.php create mode 100644 app/Models/Hypervisor.php create mode 100644 app/Models/LdapConfig.php create mode 100644 app/Models/PasswordEntry.php create mode 100644 app/Models/SecurityScan.php create mode 100644 app/Models/SecurityScore.php create mode 100644 app/Models/Setting.php create mode 100644 app/Models/Stack.php create mode 100644 app/Models/User.php create mode 100644 app/Models/VirtualMachine.php create mode 100644 app/Models/VmTemplate.php create mode 100644 app/Models/WikiPage.php create mode 100644 app/Models/WikiPageRevision.php create mode 100644 app/Models/WikiSpace.php create mode 100644 app/Providers/AppServiceProvider.php create mode 100644 app/Providers/AuthServiceProvider.php create mode 100644 app/Services/Container/DockerManager.php create mode 100644 app/Services/Hypervisor/HyperVDriver.php create mode 100644 app/Services/Hypervisor/HypervisorDriverInterface.php create mode 100644 app/Services/Hypervisor/HypervisorManager.php create mode 100644 app/Services/Hypervisor/ProxmoxDriver.php create mode 100644 app/Services/Hypervisor/VCenterDriver.php create mode 100644 bootstrap/app.php create mode 100644 bootstrap/providers.php create mode 100644 composer.json create mode 100644 config/activitylog.php create mode 100644 config/app.php create mode 100644 config/auth.php create mode 100644 config/cache.php create mode 100644 config/database.php create mode 100644 config/ldap.php create mode 100644 config/permission.php create mode 100644 config/queue.php create mode 100644 config/session.php create mode 100644 database/migrations/0001_01_01_000000_create_users_table.php create mode 100644 database/migrations/0001_01_01_000001_create_cache_table.php create mode 100644 database/migrations/0001_01_01_000002_create_jobs_table.php create mode 100644 database/migrations/2024_01_01_000001_create_settings_table.php create mode 100644 database/migrations/2024_01_01_000002_create_hypervisors_table.php create mode 100644 database/migrations/2024_01_01_000003_create_containers_table.php create mode 100644 database/migrations/2024_01_01_000004_create_cloud_table.php create mode 100644 database/migrations/2024_01_01_000005_create_security_table.php create mode 100644 database/migrations/2024_01_01_000006_create_backups_table.php create mode 100644 database/migrations/2024_01_01_000007_create_monitoring_table.php create mode 100644 database/migrations/2024_01_01_000008_create_identity_table.php create mode 100644 database/migrations/2024_01_01_000009_create_wiki_table.php create mode 100644 database/migrations/2024_01_01_000010_create_ldap_table.php create mode 100644 database/seeders/DatabaseSeeder.php create mode 100644 docker-compose.yml create mode 100644 docker/nginx/default.conf create mode 100644 docker/php/Dockerfile create mode 100644 docker/php/opcache.ini create mode 100644 docker/php/php.ini create mode 100644 docker/supervisor/supervisord.conf create mode 100644 package.json create mode 100644 resources/css/app.css create mode 100644 resources/js/Components/ui/Button.jsx create mode 100644 resources/js/Components/ui/DataTable.jsx create mode 100644 resources/js/Components/ui/GlassCard.jsx create mode 100644 resources/js/Components/ui/Modal.jsx create mode 100644 resources/js/Components/ui/ProgressBar.jsx create mode 100644 resources/js/Components/ui/StatusBadge.jsx create mode 100644 resources/js/Layouts/AppLayout.jsx create mode 100644 resources/js/Pages/Acl/Index.jsx create mode 100644 resources/js/Pages/Admin/Index.jsx create mode 100644 resources/js/Pages/Admin/Ldap.jsx create mode 100644 resources/js/Pages/Admin/Settings.jsx create mode 100644 resources/js/Pages/Auth/Login.jsx create mode 100644 resources/js/Pages/Backup/Index.jsx create mode 100644 resources/js/Pages/Cloud/Index.jsx create mode 100644 resources/js/Pages/Container/Index.jsx create mode 100644 resources/js/Pages/Dashboard/Index.jsx create mode 100644 resources/js/Pages/Error.jsx create mode 100644 resources/js/Pages/Hypervisor/Create.jsx create mode 100644 resources/js/Pages/Hypervisor/Index.jsx create mode 100644 resources/js/Pages/Identity/Index.jsx create mode 100644 resources/js/Pages/Monitoring/Index.jsx create mode 100644 resources/js/Pages/Security/Index.jsx create mode 100644 resources/js/Pages/Wiki/Index.jsx create mode 100644 resources/js/app.jsx create mode 100644 resources/js/bootstrap.js create mode 100644 resources/js/lib/utils.js create mode 100644 resources/views/app.blade.php create mode 100644 routes/api.php create mode 100644 routes/console.php create mode 100644 routes/web.php create mode 100644 setup.ps1 create mode 100644 setup.sh create mode 100644 tailwind.config.js create mode 100644 vite.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a677acd --- /dev/null +++ b/.env.example @@ -0,0 +1,69 @@ +APP_NAME="Infra Manager" +APP_ENV=local +APP_KEY= +APP_DEBUG=true +APP_VERSION=1.0.0 +APP_URL=http://localhost:8080 +APP_PORT=8080 +APP_HTTPS_PORT=8443 + +LOG_CHANNEL=stack +LOG_DEPRECATIONS_CHANNEL=null +LOG_LEVEL=debug + +# ─── Database ───────────────────────────────────────────────── +# SQLite (default — ready to use, no setup needed) +DB_CONNECTION=sqlite +DB_DATABASE=/var/www/html/database/database.sqlite + +# MySQL (uncomment to switch — run: docker compose --profile mysql up) +# DB_CONNECTION=mysql +# DB_HOST=mysql +# DB_PORT=3306 +# DB_DATABASE=infra_manager +# DB_USERNAME=infra_user +# DB_PASSWORD=infra_secret + +# ─── Redis ──────────────────────────────────────────────────── +REDIS_HOST=redis +REDIS_PASSWORD=infra_redis_secret +REDIS_PORT=6379 + +# ─── Cache & Queue ──────────────────────────────────────────── +CACHE_STORE=redis +QUEUE_CONNECTION=redis +SESSION_DRIVER=redis +SESSION_LIFETIME=480 + +# ─── Mail ───────────────────────────────────────────────────── +MAIL_MAILER=log +MAIL_HOST=mailpit +MAIL_PORT=1025 +MAIL_USERNAME=null +MAIL_PASSWORD=null +MAIL_ENCRYPTION=null +MAIL_FROM_ADDRESS="noreply@infra.local" +MAIL_FROM_NAME="${APP_NAME}" + +# ─── Security ───────────────────────────────────────────────── +# Generate with: php artisan key:generate +APP_KEY= + +# ─── Docker Config ──────────────────────────────────────────── +USER_ID=1000 +GROUP_ID=1000 +MYSQL_ROOT_PASSWORD=root_secret_change_me +MYSQL_DATABASE=infra_manager +MYSQL_USER=infra_user +MYSQL_PASSWORD=infra_secret_change_me + +# ─── LDAP (optional) ───────────────────────────────────────── +LDAP_HOST= +LDAP_PORT=389 +LDAP_BASE_DN= +LDAP_USERNAME= +LDAP_PASSWORD= +LDAP_AUTH_ENABLED=false + +# ─── Vite ───────────────────────────────────────────────────── +VITE_APP_NAME="${APP_NAME}" diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ee1ae57 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +/node_modules +/public/build +/public/hot +/public/storage +/storage/*.key +/vendor +.env +.env.backup +.phpunit.result.cache +database/database.sqlite +Homestead.json +Homestead.yaml +auth.json +npm-debug.log +yarn-error.log +/.fleet +/.idea +/.vscode +docker/nginx/ssl/ +/bootstrap/cache/*.php diff --git a/app/Http/Controllers/Acl/RoleController.php b/app/Http/Controllers/Acl/RoleController.php new file mode 100644 index 0000000..6325368 --- /dev/null +++ b/app/Http/Controllers/Acl/RoleController.php @@ -0,0 +1,58 @@ + \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.'); + } +} diff --git a/app/Http/Controllers/Acl/UserController.php b/app/Http/Controllers/Acl/UserController.php new file mode 100644 index 0000000..bff1b6d --- /dev/null +++ b/app/Http/Controllers/Acl/UserController.php @@ -0,0 +1,93 @@ +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.'); + } +} diff --git a/app/Http/Controllers/Admin/AdminController.php b/app/Http/Controllers/Admin/AdminController.php new file mode 100644 index 0000000..015ae7f --- /dev/null +++ b/app/Http/Controllers/Admin/AdminController.php @@ -0,0 +1,108 @@ + [ + '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]; + } +} diff --git a/app/Http/Controllers/Admin/LdapController.php b/app/Http/Controllers/Admin/LdapController.php new file mode 100644 index 0000000..958731c --- /dev/null +++ b/app/Http/Controllers/Admin/LdapController.php @@ -0,0 +1,83 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Admin/SettingsController.php b/app/Http/Controllers/Admin/SettingsController.php new file mode 100644 index 0000000..53ceac2 --- /dev/null +++ b/app/Http/Controllers/Admin/SettingsController.php @@ -0,0 +1,27 @@ +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.'); + } +} diff --git a/app/Http/Controllers/Api/MetricsController.php b/app/Http/Controllers/Api/MetricsController.php new file mode 100644 index 0000000..dae1d03 --- /dev/null +++ b/app/Http/Controllers/Api/MetricsController.php @@ -0,0 +1,21 @@ +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(), + ]); + } +} diff --git a/app/Http/Controllers/Api/WebSocketController.php b/app/Http/Controllers/Api/WebSocketController.php new file mode 100644 index 0000000..9cc9d3d --- /dev/null +++ b/app/Http/Controllers/Api/WebSocketController.php @@ -0,0 +1,15 @@ +json(['ticket' => base64_encode(random_bytes(32))]); + } +} diff --git a/app/Http/Controllers/Auth/AuthController.php b/app/Http/Controllers/Auth/AuthController.php new file mode 100644 index 0000000..47b5c0d --- /dev/null +++ b/app/Http/Controllers/Auth/AuthController.php @@ -0,0 +1,83 @@ + 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'); + } +} diff --git a/app/Http/Controllers/Backup/BackupController.php b/app/Http/Controllers/Backup/BackupController.php new file mode 100644 index 0000000..6606d96 --- /dev/null +++ b/app/Http/Controllers/Backup/BackupController.php @@ -0,0 +1,52 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Backup/RestoreController.php b/app/Http/Controllers/Backup/RestoreController.php new file mode 100644 index 0000000..b69e5d4 --- /dev/null +++ b/app/Http/Controllers/Backup/RestoreController.php @@ -0,0 +1,19 @@ +validate([ + 'restore_mode' => 'required|in:overwrite,new,test', + ]); + // Dispatch restore job + return back()->with('success', 'Restore initiated.'); + } +} diff --git a/app/Http/Controllers/Cloud/CloudAccountController.php b/app/Http/Controllers/Cloud/CloudAccountController.php new file mode 100644 index 0000000..43be618 --- /dev/null +++ b/app/Http/Controllers/Cloud/CloudAccountController.php @@ -0,0 +1,69 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Cloud/CloudResourceController.php b/app/Http/Controllers/Cloud/CloudResourceController.php new file mode 100644 index 0000000..557ec27 --- /dev/null +++ b/app/Http/Controllers/Cloud/CloudResourceController.php @@ -0,0 +1,22 @@ + CloudResource::with('account')->paginate(50), + ]); + } + + public function show(CloudResource $resource) + { + return Inertia::render('Cloud/ResourceShow', ['resource' => $resource->load('account')]); + } +} diff --git a/app/Http/Controllers/Container/ContainerController.php b/app/Http/Controllers/Container/ContainerController.php new file mode 100644 index 0000000..62961f9 --- /dev/null +++ b/app/Http/Controllers/Container/ContainerController.php @@ -0,0 +1,54 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Container/StackController.php b/app/Http/Controllers/Container/StackController.php new file mode 100644 index 0000000..9f493de --- /dev/null +++ b/app/Http/Controllers/Container/StackController.php @@ -0,0 +1,68 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/DashboardController.php b/app/Http/Controllers/DashboardController.php new file mode 100644 index 0000000..e23d60b --- /dev/null +++ b/app/Http/Controllers/DashboardController.php @@ -0,0 +1,68 @@ +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, + ]); + } +} diff --git a/app/Http/Controllers/Hypervisor/HypervisorController.php b/app/Http/Controllers/Hypervisor/HypervisorController.php new file mode 100644 index 0000000..3cf99d7 --- /dev/null +++ b/app/Http/Controllers/Hypervisor/HypervisorController.php @@ -0,0 +1,121 @@ +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); + } +} diff --git a/app/Http/Controllers/Hypervisor/TemplateController.php b/app/Http/Controllers/Hypervisor/TemplateController.php new file mode 100644 index 0000000..0ab02af --- /dev/null +++ b/app/Http/Controllers/Hypervisor/TemplateController.php @@ -0,0 +1,77 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Hypervisor/VirtualMachineController.php b/app/Http/Controllers/Hypervisor/VirtualMachineController.php new file mode 100644 index 0000000..8a80638 --- /dev/null +++ b/app/Http/Controllers/Hypervisor/VirtualMachineController.php @@ -0,0 +1,60 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Identity/CertificateController.php b/app/Http/Controllers/Identity/CertificateController.php new file mode 100644 index 0000000..ccc8031 --- /dev/null +++ b/app/Http/Controllers/Identity/CertificateController.php @@ -0,0 +1,69 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Identity/PasswordController.php b/app/Http/Controllers/Identity/PasswordController.php new file mode 100644 index 0000000..ea977b9 --- /dev/null +++ b/app/Http/Controllers/Identity/PasswordController.php @@ -0,0 +1,60 @@ + 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.'); + } +} diff --git a/app/Http/Controllers/Monitoring/MonitoringController.php b/app/Http/Controllers/Monitoring/MonitoringController.php new file mode 100644 index 0000000..79d9eaf --- /dev/null +++ b/app/Http/Controllers/Monitoring/MonitoringController.php @@ -0,0 +1,58 @@ + 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'); + } +} diff --git a/app/Http/Controllers/Security/ScanController.php b/app/Http/Controllers/Security/ScanController.php new file mode 100644 index 0000000..78fa70e --- /dev/null +++ b/app/Http/Controllers/Security/ScanController.php @@ -0,0 +1,44 @@ + 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]); + } +} diff --git a/app/Http/Controllers/Security/SecurityDashboardController.php b/app/Http/Controllers/Security/SecurityDashboardController.php new file mode 100644 index 0000000..5dc995a --- /dev/null +++ b/app/Http/Controllers/Security/SecurityDashboardController.php @@ -0,0 +1,39 @@ +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'); + } +} diff --git a/app/Http/Controllers/Wiki/DocumentController.php b/app/Http/Controllers/Wiki/DocumentController.php new file mode 100644 index 0000000..dd5f539 --- /dev/null +++ b/app/Http/Controllers/Wiki/DocumentController.php @@ -0,0 +1,96 @@ +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.'); + } +} diff --git a/app/Http/Controllers/Wiki/WikiController.php b/app/Http/Controllers/Wiki/WikiController.php new file mode 100644 index 0000000..8206566 --- /dev/null +++ b/app/Http/Controllers/Wiki/WikiController.php @@ -0,0 +1,54 @@ + 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.'); + } +} diff --git a/app/Http/Middleware/HandleInertiaRequests.php b/app/Http/Middleware/HandleInertiaRequests.php new file mode 100644 index 0000000..a372181 --- /dev/null +++ b/app/Http/Middleware/HandleInertiaRequests.php @@ -0,0 +1,74 @@ +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; + } +} diff --git a/app/Models/Alert.php b/app/Models/Alert.php new file mode 100644 index 0000000..b6a6ec8 --- /dev/null +++ b/app/Models/Alert.php @@ -0,0 +1,22 @@ + 'datetime', + 'resolved_at' => 'datetime', + ]; + + public function rule() { return $this->belongsTo(AlertRule::class); } + public function acknowledgedBy() { return $this->belongsTo(User::class, 'acknowledged_by'); } +} diff --git a/app/Models/AlertRule.php b/app/Models/AlertRule.php new file mode 100644 index 0000000..42f2c2d --- /dev/null +++ b/app/Models/AlertRule.php @@ -0,0 +1,14 @@ + 'array', 'is_active' => 'boolean']; +} diff --git a/app/Models/Backup.php b/app/Models/Backup.php new file mode 100644 index 0000000..c00d3d6 --- /dev/null +++ b/app/Models/Backup.php @@ -0,0 +1,16 @@ + 'array', 'expires_at' => 'datetime', 'started_at' => 'datetime', 'completed_at' => 'datetime']; + public function policy() { return $this->belongsTo(BackupPolicy::class); } +} diff --git a/app/Models/BackupPolicy.php b/app/Models/BackupPolicy.php new file mode 100644 index 0000000..9b913af --- /dev/null +++ b/app/Models/BackupPolicy.php @@ -0,0 +1,15 @@ + 'array', 'storage_config' => 'array', 'compression' => 'array', 'encryption' => 'array', 'is_active' => 'boolean']; + public function backups() { return $this->hasMany(Backup::class, 'policy_id'); } +} diff --git a/app/Models/Certificate.php b/app/Models/Certificate.php new file mode 100644 index 0000000..fac8fb5 --- /dev/null +++ b/app/Models/Certificate.php @@ -0,0 +1,34 @@ + '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'); } +} diff --git a/app/Models/CloudAccount.php b/app/Models/CloudAccount.php new file mode 100644 index 0000000..fab1b18 --- /dev/null +++ b/app/Models/CloudAccount.php @@ -0,0 +1,40 @@ + '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'); } +} diff --git a/app/Models/CloudResource.php b/app/Models/CloudResource.php new file mode 100644 index 0000000..3cce851 --- /dev/null +++ b/app/Models/CloudResource.php @@ -0,0 +1,15 @@ + 'array', 'tags' => 'array']; + public function account() { return $this->belongsTo(CloudAccount::class); } +} diff --git a/app/Models/Container.php b/app/Models/Container.php new file mode 100644 index 0000000..22337f2 --- /dev/null +++ b/app/Models/Container.php @@ -0,0 +1,23 @@ + 'array', + 'volumes' => 'array', + 'env' => 'array', + 'labels' => 'array', + 'networks' => 'array', + ]; + + public function endpoint() { return $this->belongsTo(DockerEndpoint::class); } +} diff --git a/app/Models/DockerEndpoint.php b/app/Models/DockerEndpoint.php new file mode 100644 index 0000000..25b37ad --- /dev/null +++ b/app/Models/DockerEndpoint.php @@ -0,0 +1,29 @@ + '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'); } +} diff --git a/app/Models/Hypervisor.php b/app/Models/Hypervisor.php new file mode 100644 index 0000000..5e82a1a --- /dev/null +++ b/app/Models/Hypervisor.php @@ -0,0 +1,70 @@ + '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), + }; + } +} diff --git a/app/Models/LdapConfig.php b/app/Models/LdapConfig.php new file mode 100644 index 0000000..ae6e1f5 --- /dev/null +++ b/app/Models/LdapConfig.php @@ -0,0 +1,35 @@ + '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; + } +} diff --git a/app/Models/PasswordEntry.php b/app/Models/PasswordEntry.php new file mode 100644 index 0000000..786f901 --- /dev/null +++ b/app/Models/PasswordEntry.php @@ -0,0 +1,40 @@ + '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); } +} diff --git a/app/Models/SecurityScan.php b/app/Models/SecurityScan.php new file mode 100644 index 0000000..146ff72 --- /dev/null +++ b/app/Models/SecurityScan.php @@ -0,0 +1,19 @@ + 'array', + 'started_at' => 'datetime', + 'completed_at' => 'datetime', + ]; +} diff --git a/app/Models/SecurityScore.php b/app/Models/SecurityScore.php new file mode 100644 index 0000000..fb962ae --- /dev/null +++ b/app/Models/SecurityScore.php @@ -0,0 +1,11 @@ + 'array', 'calculated_at' => 'datetime']; +} diff --git a/app/Models/Setting.php b/app/Models/Setting.php new file mode 100644 index 0000000..9e0d3c2 --- /dev/null +++ b/app/Models/Setting.php @@ -0,0 +1,57 @@ + '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, + ]); + } +} diff --git a/app/Models/Stack.php b/app/Models/Stack.php new file mode 100644 index 0000000..71214e0 --- /dev/null +++ b/app/Models/Stack.php @@ -0,0 +1,15 @@ + 'array']; + public function endpoint() { return $this->belongsTo(DockerEndpoint::class); } + public function createdBy() { return $this->belongsTo(User::class, 'created_by'); } +} diff --git a/app/Models/User.php b/app/Models/User.php new file mode 100644 index 0000000..2b96fcb --- /dev/null +++ b/app/Models/User.php @@ -0,0 +1,58 @@ + '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); + } +} diff --git a/app/Models/VirtualMachine.php b/app/Models/VirtualMachine.php new file mode 100644 index 0000000..045991e --- /dev/null +++ b/app/Models/VirtualMachine.php @@ -0,0 +1,23 @@ + 'array', + 'metadata' => 'array', + ]; + + public function hypervisor() { return $this->belongsTo(Hypervisor::class); } + public function createdBy() { return $this->belongsTo(User::class, 'created_by'); } +} diff --git a/app/Models/VmTemplate.php b/app/Models/VmTemplate.php new file mode 100644 index 0000000..27237f7 --- /dev/null +++ b/app/Models/VmTemplate.php @@ -0,0 +1,19 @@ + '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'); } +} diff --git a/app/Models/WikiPage.php b/app/Models/WikiPage.php new file mode 100644 index 0000000..bdd4239 --- /dev/null +++ b/app/Models/WikiPage.php @@ -0,0 +1,25 @@ + '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'); } +} diff --git a/app/Models/WikiPageRevision.php b/app/Models/WikiPageRevision.php new file mode 100644 index 0000000..a50bbf9 --- /dev/null +++ b/app/Models/WikiPageRevision.php @@ -0,0 +1,12 @@ +belongsTo(WikiPage::class); } + public function createdBy() { return $this->belongsTo(User::class, 'created_by'); } +} diff --git a/app/Models/WikiSpace.php b/app/Models/WikiSpace.php new file mode 100644 index 0000000..d0f932b --- /dev/null +++ b/app/Models/WikiSpace.php @@ -0,0 +1,18 @@ + 'boolean']; + + public function pages() { return $this->hasMany(WikiPage::class, 'space_id'); } + public function createdBy() { return $this->belongsTo(User::class, 'created_by'); } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php new file mode 100644 index 0000000..d18396b --- /dev/null +++ b/app/Providers/AppServiceProvider.php @@ -0,0 +1,42 @@ +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; + }); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php new file mode 100644 index 0000000..b2549de --- /dev/null +++ b/app/Providers/AuthServiceProvider.php @@ -0,0 +1,25 @@ +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; + } +} diff --git a/app/Services/Hypervisor/HyperVDriver.php b/app/Services/Hypervisor/HyperVDriver.php new file mode 100644 index 0000000..1bfa7b9 --- /dev/null +++ b/app/Services/Hypervisor/HyperVDriver.php @@ -0,0 +1,32 @@ + '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 []; } +} diff --git a/app/Services/Hypervisor/HypervisorDriverInterface.php b/app/Services/Hypervisor/HypervisorDriverInterface.php new file mode 100644 index 0000000..fbc4858 --- /dev/null +++ b/app/Services/Hypervisor/HypervisorDriverInterface.php @@ -0,0 +1,19 @@ +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()]); + } + }); + } +} diff --git a/app/Services/Hypervisor/ProxmoxDriver.php b/app/Services/Hypervisor/ProxmoxDriver.php new file mode 100644 index 0000000..77c0964 --- /dev/null +++ b/app/Services/Hypervisor/ProxmoxDriver.php @@ -0,0 +1,179 @@ +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); + } +} diff --git a/app/Services/Hypervisor/VCenterDriver.php b/app/Services/Hypervisor/VCenterDriver.php new file mode 100644 index 0000000..4c97295 --- /dev/null +++ b/app/Services/Hypervisor/VCenterDriver.php @@ -0,0 +1,163 @@ +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) ?? []; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php new file mode 100644 index 0000000..0a6194c --- /dev/null +++ b/bootstrap/app.php @@ -0,0 +1,41 @@ +withRouting( + web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', + commands: __DIR__.'/../routes/console.php', + health: '/up', + ) + ->withMiddleware(function (Middleware $middleware) { + $middleware->web(append: [ + HandleInertiaRequests::class, + ]); + + $middleware->alias([ + 'permission' => \Spatie\LaravelPermission\Middleware\PermissionMiddleware::class, + 'role' => \Spatie\LaravelPermission\Middleware\RoleMiddleware::class, + 'role_or_permission' => \Spatie\LaravelPermission\Middleware\RoleOrPermissionMiddleware::class, + ]); + }) + ->withExceptions(function (Exceptions $exceptions) { + $exceptions->respond(function (\Symfony\Component\HttpFoundation\Response $response, \Throwable $e, Request $request) { + if (!app()->environment(['local', 'testing']) && in_array($response->getStatusCode(), [500, 503, 404, 403])) { + return Inertia::render('Error', ['status' => $response->getStatusCode()]) + ->toResponse($request) + ->setStatusCode($response->getStatusCode()); + } + if ($response->getStatusCode() === 419) { + return back()->with('error', 'The page expired, please try again.'); + } + return $response; + }); + })->create(); diff --git a/bootstrap/providers.php b/bootstrap/providers.php new file mode 100644 index 0000000..51b4215 --- /dev/null +++ b/bootstrap/providers.php @@ -0,0 +1,8 @@ + env('ACTIVITY_LOGGER_ENABLED', true), + 'delete_records_older_than_days' => 365, + 'default_log_name' => 'default', + 'default_auth_driver' => null, + 'subject_returns_soft_deleted_models' => false, + 'activity_model' => \Spatie\Activitylog\Models\Activity::class, + 'table_name' => env('ACTIVITY_LOG_TABLE', 'activity_log'), + 'database_connection' => env('ACTIVITY_LOG_DB_CONNECTION'), +]; diff --git a/config/app.php b/config/app.php new file mode 100644 index 0000000..7a3a156 --- /dev/null +++ b/config/app.php @@ -0,0 +1,45 @@ + env('APP_NAME', 'Infra Manager'), + 'version' => env('APP_VERSION', '1.0.0'), + 'env' => env('APP_ENV', 'production'), + 'debug' => (bool) env('APP_DEBUG', false), + 'url' => env('APP_URL', 'http://localhost'), + 'timezone' => 'UTC', + 'locale' => 'en', + 'fallback_locale' => 'en', + 'faker_locale' => 'en_US', + 'key' => env('APP_KEY'), + 'cipher' => 'AES-256-CBC', + 'maintenance' => ['driver' => 'file'], + 'providers' => [ + Illuminate\Auth\AuthServiceProvider::class, + Illuminate\Broadcasting\BroadcastServiceProvider::class, + Illuminate\Bus\BusServiceProvider::class, + Illuminate\Cache\CacheServiceProvider::class, + Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class, + Illuminate\Cookie\CookieServiceProvider::class, + Illuminate\Database\DatabaseServiceProvider::class, + Illuminate\Encryption\EncryptionServiceProvider::class, + Illuminate\Filesystem\FilesystemServiceProvider::class, + Illuminate\Foundation\Providers\FoundationServiceProvider::class, + Illuminate\Hashing\HashServiceProvider::class, + Illuminate\Mail\MailServiceProvider::class, + Illuminate\Notifications\NotificationServiceProvider::class, + Illuminate\Pagination\PaginationServiceProvider::class, + Illuminate\Pipeline\PipelineServiceProvider::class, + Illuminate\Queue\QueueServiceProvider::class, + Illuminate\Redis\RedisServiceProvider::class, + Illuminate\Auth\Passwords\PasswordResetServiceProvider::class, + Illuminate\Session\SessionServiceProvider::class, + Illuminate\Translation\TranslationServiceProvider::class, + Illuminate\Validation\ValidationServiceProvider::class, + Illuminate\View\ViewServiceProvider::class, + Tightenco\Ziggy\ZiggyServiceProvider::class, + Spatie\Activitylog\ActivitylogServiceProvider::class, + ], + 'aliases' => Illuminate\Support\Facades\Facade::defaultAliases()->merge([ + 'Route' => Illuminate\Support\Facades\Route::class, + ])->toArray(), +]; diff --git a/config/auth.php b/config/auth.php new file mode 100644 index 0000000..11d5210 --- /dev/null +++ b/config/auth.php @@ -0,0 +1,20 @@ + ['guard' => 'web', 'passwords' => 'users'], + + 'guards' => [ + 'web' => ['driver' => 'session', 'provider' => 'users'], + 'api' => ['driver' => 'sanctum', 'provider' => 'users'], + ], + + 'providers' => [ + 'users' => ['driver' => 'eloquent', 'model' => App\Models\User::class], + ], + + 'passwords' => [ + 'users' => ['provider' => 'users', 'table' => 'password_reset_tokens', 'expire' => 60, 'throttle' => 60], + ], + + 'password_timeout' => 10800, +]; diff --git a/config/cache.php b/config/cache.php new file mode 100644 index 0000000..6cd3a7f --- /dev/null +++ b/config/cache.php @@ -0,0 +1,11 @@ + env('CACHE_STORE', 'redis'), + 'stores' => [ + 'redis' => ['driver' => 'redis', 'connection' => 'cache', 'lock_connection' => 'default'], + 'file' => ['driver' => 'file', 'path' => storage_path('framework/cache/data'), 'lock_path' => storage_path('framework/cache/data')], + 'array' => ['driver' => 'array', 'serialize' => false], + ], + 'prefix' => env('CACHE_PREFIX', 'infra_cache_'), +]; diff --git a/config/database.php b/config/database.php new file mode 100644 index 0000000..07e8fad --- /dev/null +++ b/config/database.php @@ -0,0 +1,47 @@ + env('DB_CONNECTION', 'sqlite'), + + 'connections' => [ + 'sqlite' => [ + 'driver' => 'sqlite', + 'url' => env('DB_URL'), + 'database' => env('DB_DATABASE', database_path('database.sqlite')), + 'prefix' => '', + 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true), + 'busy_timeout' => null, + 'journal_mode' => null, + 'synchronous' => null, + ], + + 'mysql' => [ + 'driver' => 'mysql', + 'url' => env('DB_URL'), + 'host' => env('DB_HOST', '127.0.0.1'), + 'port' => env('DB_PORT', '3306'), + 'database' => env('DB_DATABASE', 'infra_manager'), + 'username' => env('DB_USERNAME', 'root'), + 'password' => env('DB_PASSWORD', ''), + 'unix_socket' => env('DB_SOCKET', ''), + 'charset' => env('DB_CHARSET', 'utf8mb4'), + 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'), + 'prefix' => '', + 'prefix_indexes' => true, + 'strict' => true, + 'engine' => null, + 'options' => extension_loaded('pdo_mysql') ? array_filter([ + PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'), + ]) : [], + ], + ], + + 'migrations' => ['table' => 'migrations', 'update_date_on_publish' => true], + + 'redis' => [ + 'client' => env('REDIS_CLIENT', 'phpredis'), + 'options' => ['cluster' => env('REDIS_CLUSTER', 'redis'), 'prefix' => env('REDIS_PREFIX', 'infra_')], + 'default' => ['url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_DB', '0')], + 'cache' => ['url' => env('REDIS_URL'), 'host' => env('REDIS_HOST', '127.0.0.1'), 'username' => env('REDIS_USERNAME'), 'password' => env('REDIS_PASSWORD'), 'port' => env('REDIS_PORT', '6379'), 'database' => env('REDIS_CACHE_DB', '1')], + ], +]; diff --git a/config/ldap.php b/config/ldap.php new file mode 100644 index 0000000..2d45db1 --- /dev/null +++ b/config/ldap.php @@ -0,0 +1,36 @@ + env('LDAP_CONNECTION', 'default'), + + 'connections' => [ + 'default' => [ + 'hosts' => [env('LDAP_HOST', '127.0.0.1')], + 'username' => env('LDAP_USERNAME', ''), + 'password' => env('LDAP_PASSWORD', ''), + 'port' => (int) env('LDAP_PORT', 389), + 'base_dn' => env('LDAP_BASE_DN', ''), + 'timeout' => 5, + 'use_ssl' => env('LDAP_SSL', false), + 'use_tls' => env('LDAP_TLS', false), + 'use_sasl' => env('LDAP_SASL', false), + 'version' => 3, + 'follow_referrals' => false, + ], + ], + + 'logging' => [ + 'enabled' => env('LDAP_LOGGING', env('APP_DEBUG', false)), + 'channel' => env('LOG_CHANNEL', 'stack'), + ], + + 'cache' => [ + 'enabled' => env('LDAP_CACHE', false), + 'driver' => env('LDAP_CACHE_DRIVER', 'file'), + 'lifetime' => env('LDAP_CACHE_LIFETIME', 3600), + ], + + 'auth' => [ + 'enabled' => env('LDAP_AUTH_ENABLED', false), + ], +]; diff --git a/config/permission.php b/config/permission.php new file mode 100644 index 0000000..58e5547 --- /dev/null +++ b/config/permission.php @@ -0,0 +1,33 @@ + [ + 'permission' => Spatie\Permission\Models\Permission::class, + 'role' => Spatie\Permission\Models\Role::class, + ], + 'table_names' => [ + 'roles' => 'roles', + 'permissions' => 'permissions', + 'model_has_permissions' => 'model_has_permissions', + 'model_has_roles' => 'model_has_roles', + 'role_has_permissions' => 'role_has_permissions', + ], + 'column_names' => [ + 'role_pivot_key' => null, + 'permission_pivot_key' => null, + 'model_morph_key' => 'model_id', + 'team_foreign_key' => 'team_id', + ], + 'register_permission_check_method' => true, + 'register_octane_reset_listener' => false, + 'teams' => false, + 'use_passport_client_credentials' => false, + 'display_permission_in_exception' => false, + 'display_role_in_exception' => false, + 'enable_wildcard_permission' => false, + 'cache' => [ + 'expiration_time' => \DateInterval::createFromDateString('24 hours'), + 'key' => 'spatie.permission.cache', + 'store' => 'default', + ], +]; diff --git a/config/queue.php b/config/queue.php new file mode 100644 index 0000000..8bb88ef --- /dev/null +++ b/config/queue.php @@ -0,0 +1,12 @@ + env('QUEUE_CONNECTION', 'redis'), + 'connections' => [ + 'sync' => ['driver' => 'sync'], + 'database' => ['driver' => 'database', 'connection' => env('DB_QUEUE_CONNECTION'), 'table' => env('DB_QUEUE_TABLE', 'jobs'), 'queue' => env('DB_QUEUE', 'default'), 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90), 'after_commit' => false], + 'redis' => ['driver' => 'redis', 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'), 'queue' => env('REDIS_QUEUE', 'default'), 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90), 'block_for' => null, 'after_commit' => false], + ], + 'batching' => ['database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'job_batches'], + 'failed' => ['driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'), 'database' => env('DB_CONNECTION', 'sqlite'), 'table' => 'failed_jobs'], +]; diff --git a/config/session.php b/config/session.php new file mode 100644 index 0000000..249b1bc --- /dev/null +++ b/config/session.php @@ -0,0 +1,20 @@ + env('SESSION_DRIVER', 'redis'), + 'lifetime' => env('SESSION_LIFETIME', 480), + 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false), + 'encrypt' => env('SESSION_ENCRYPT', false), + 'files' => storage_path('framework/sessions'), + 'connection' => env('SESSION_CONNECTION'), + 'table' => env('SESSION_TABLE', 'sessions'), + 'store' => env('SESSION_STORE'), + 'lottery' => [2, 100], + 'cookie' => env('SESSION_COOKIE', 'infra_session'), + 'path' => env('SESSION_PATH', '/'), + 'domain' => env('SESSION_DOMAIN'), + 'secure' => env('SESSION_SECURE_COOKIE', false), + 'http_only' => env('SESSION_HTTP_ONLY', true), + 'same_site' => env('SESSION_SAME_SITE', 'lax'), + 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false), +]; diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php new file mode 100644 index 0000000..8c2d8fd --- /dev/null +++ b/database/migrations/0001_01_01_000000_create_users_table.php @@ -0,0 +1,55 @@ +id(); + $table->string('name'); + $table->string('email')->unique(); + $table->timestamp('email_verified_at')->nullable(); + $table->string('password')->nullable(); // nullable for LDAP users + $table->string('avatar')->nullable(); + $table->string('auth_source')->default('local'); // local | ldap + $table->string('ldap_guid')->nullable()->unique(); + $table->string('ldap_domain')->nullable(); + $table->boolean('is_active')->default(true); + $table->boolean('mfa_enabled')->default(false); + $table->string('mfa_secret')->nullable(); + $table->json('mfa_recovery_codes')->nullable(); + $table->timestamp('last_login_at')->nullable(); + $table->string('last_login_ip')->nullable(); + $table->json('preferences')->nullable(); // theme, language, etc. + $table->rememberToken(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('password_reset_tokens', function (Blueprint $table) { + $table->string('email')->primary(); + $table->string('token'); + $table->timestamp('created_at')->nullable(); + }); + + Schema::create('sessions', function (Blueprint $table) { + $table->string('id')->primary(); + $table->foreignId('user_id')->nullable()->index(); + $table->string('ip_address', 45)->nullable(); + $table->text('user_agent')->nullable(); + $table->longText('payload'); + $table->integer('last_activity')->index(); + }); + } + + public function down(): void + { + Schema::dropIfExists('sessions'); + Schema::dropIfExists('password_reset_tokens'); + Schema::dropIfExists('users'); + } +}; diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php new file mode 100644 index 0000000..26f163c --- /dev/null +++ b/database/migrations/0001_01_01_000001_create_cache_table.php @@ -0,0 +1,29 @@ +string('key')->primary(); + $table->mediumText('value'); + $table->integer('expiration'); + }); + + Schema::create('cache_locks', function (Blueprint $table) { + $table->string('key')->primary(); + $table->string('owner'); + $table->integer('expiration'); + }); + } + + public function down(): void + { + Schema::dropIfExists('cache_locks'); + Schema::dropIfExists('cache'); + } +}; diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php new file mode 100644 index 0000000..b09c234 --- /dev/null +++ b/database/migrations/0001_01_01_000002_create_jobs_table.php @@ -0,0 +1,51 @@ +id(); + $table->string('queue')->index(); + $table->longText('payload'); + $table->unsignedTinyInteger('attempts'); + $table->unsignedInteger('reserved_at')->nullable(); + $table->unsignedInteger('available_at'); + $table->unsignedInteger('created_at'); + }); + + Schema::create('job_batches', function (Blueprint $table) { + $table->string('id')->primary(); + $table->string('name'); + $table->integer('total_jobs'); + $table->integer('pending_jobs'); + $table->integer('failed_jobs'); + $table->longText('failed_job_ids'); + $table->mediumText('options')->nullable(); + $table->integer('cancelled_at')->nullable(); + $table->integer('created_at'); + $table->integer('finished_at')->nullable(); + }); + + Schema::create('failed_jobs', function (Blueprint $table) { + $table->id(); + $table->string('uuid')->unique(); + $table->text('connection'); + $table->text('queue'); + $table->longText('payload'); + $table->longText('exception'); + $table->timestamp('failed_at')->useCurrent(); + }); + } + + public function down(): void + { + Schema::dropIfExists('failed_jobs'); + Schema::dropIfExists('job_batches'); + Schema::dropIfExists('jobs'); + } +}; diff --git a/database/migrations/2024_01_01_000001_create_settings_table.php b/database/migrations/2024_01_01_000001_create_settings_table.php new file mode 100644 index 0000000..95b3557 --- /dev/null +++ b/database/migrations/2024_01_01_000001_create_settings_table.php @@ -0,0 +1,26 @@ +id(); + $table->string('group')->default('general'); + $table->string('key')->unique(); + $table->text('value')->nullable(); + $table->string('type')->default('string'); // string, json, boolean, integer + $table->boolean('is_encrypted')->default(false); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('settings'); + } +}; diff --git a/database/migrations/2024_01_01_000002_create_hypervisors_table.php b/database/migrations/2024_01_01_000002_create_hypervisors_table.php new file mode 100644 index 0000000..642931d --- /dev/null +++ b/database/migrations/2024_01_01_000002_create_hypervisors_table.php @@ -0,0 +1,102 @@ +id(); + $table->string('name'); + $table->string('type'); // vcenter, esxi, proxmox, hyper-v, xen, kvm + $table->string('version')->nullable(); + $table->string('host'); + $table->integer('port')->default(443); + $table->string('username'); + $table->text('password'); // encrypted + $table->boolean('ssl_verify')->default(true); + $table->string('datacenter')->nullable(); + $table->string('status')->default('unknown'); // online, offline, unknown, error + $table->json('metadata')->nullable(); + $table->timestamp('last_sync_at')->nullable(); + $table->text('last_error')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('virtual_machines', function (Blueprint $table) { + $table->id(); + $table->foreignId('hypervisor_id')->constrained('hypervisors')->cascadeOnDelete(); + $table->string('remote_id'); // ID from hypervisor + $table->string('name'); + $table->string('status'); // running, stopped, suspended, unknown + $table->string('os_type')->nullable(); + $table->string('os_version')->nullable(); + $table->integer('cpu_count')->default(0); + $table->bigInteger('memory_mb')->default(0); + $table->bigInteger('disk_gb')->default(0); + $table->string('ip_address')->nullable(); + $table->string('mac_address')->nullable(); + $table->string('host_node')->nullable(); + $table->string('cluster')->nullable(); + $table->string('datastore')->nullable(); + $table->json('tags')->nullable(); + $table->json('metadata')->nullable(); + $table->float('cpu_usage')->default(0); + $table->float('memory_usage')->default(0); + $table->foreignId('created_by')->nullable()->constrained('users'); + $table->timestamps(); + + $table->unique(['hypervisor_id', 'remote_id']); + }); + + Schema::create('vm_templates', function (Blueprint $table) { + $table->id(); + $table->foreignId('hypervisor_id')->nullable()->constrained('hypervisors')->nullOnDelete(); + $table->string('name'); + $table->string('description')->nullable(); + $table->string('os_type'); + $table->string('os_version')->nullable(); + $table->integer('cpu_count')->default(2); + $table->bigInteger('memory_mb')->default(2048); + $table->bigInteger('disk_gb')->default(50); + $table->json('network_config')->nullable(); + $table->json('customization')->nullable(); // ansible playbooks, cloud-init, etc. + $table->json('variables')->nullable(); // template variables + $table->text('provision_script')->nullable(); // post-deploy script + $table->string('status')->default('ready'); // ready, deploying, error + $table->string('remote_id')->nullable(); // template ID on hypervisor + $table->json('tags')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('vm_deployments', function (Blueprint $table) { + $table->id(); + $table->foreignId('template_id')->constrained('vm_templates'); + $table->foreignId('hypervisor_id')->constrained('hypervisors'); + $table->foreignId('virtual_machine_id')->nullable()->constrained('virtual_machines'); + $table->string('vm_name'); + $table->json('deploy_config'); + $table->string('status'); // queued, running, success, failed + $table->text('log')->nullable(); + $table->foreignId('deployed_by')->constrained('users'); + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('vm_deployments'); + Schema::dropIfExists('vm_templates'); + Schema::dropIfExists('virtual_machines'); + Schema::dropIfExists('hypervisors'); + } +}; diff --git a/database/migrations/2024_01_01_000003_create_containers_table.php b/database/migrations/2024_01_01_000003_create_containers_table.php new file mode 100644 index 0000000..f4476e1 --- /dev/null +++ b/database/migrations/2024_01_01_000003_create_containers_table.php @@ -0,0 +1,80 @@ +id(); + $table->string('name'); + $table->string('type')->default('docker'); // docker, swarm, k8s + $table->string('host'); // unix:///var/run/docker.sock or tcp://host:port + $table->boolean('tls_enabled')->default(false); + $table->text('tls_cert')->nullable(); + $table->text('tls_key')->nullable(); + $table->text('tls_ca')->nullable(); + $table->string('status')->default('unknown'); + $table->json('info')->nullable(); // Docker engine info + $table->timestamp('last_sync_at')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('containers', function (Blueprint $table) { + $table->id(); + $table->foreignId('endpoint_id')->constrained('docker_endpoints')->cascadeOnDelete(); + $table->string('container_id', 64); // Docker container ID + $table->string('name'); + $table->string('image'); + $table->string('image_id')->nullable(); + $table->string('status'); // running, stopped, paused, exited, dead + $table->string('state')->nullable(); + $table->json('ports')->nullable(); + $table->json('volumes')->nullable(); + $table->json('env')->nullable(); + $table->json('labels')->nullable(); + $table->json('networks')->nullable(); + $table->bigInteger('created_at_unix')->nullable(); + $table->timestamps(); + + $table->unique(['endpoint_id', 'container_id']); + }); + + Schema::create('stacks', function (Blueprint $table) { + $table->id(); + $table->foreignId('endpoint_id')->constrained('docker_endpoints')->cascadeOnDelete(); + $table->string('name'); + $table->string('type')->default('compose'); // compose, swarm-stack + $table->text('compose_content'); // docker-compose.yml content + $table->json('env_vars')->nullable(); + $table->string('status')->default('stopped'); + $table->string('remote_id')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('docker_images', function (Blueprint $table) { + $table->id(); + $table->foreignId('endpoint_id')->constrained('docker_endpoints')->cascadeOnDelete(); + $table->string('image_id', 71); + $table->json('repo_tags')->nullable(); + $table->bigInteger('size')->default(0); + $table->bigInteger('created_at_unix')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('docker_images'); + Schema::dropIfExists('stacks'); + Schema::dropIfExists('containers'); + Schema::dropIfExists('docker_endpoints'); + } +}; diff --git a/database/migrations/2024_01_01_000004_create_cloud_table.php b/database/migrations/2024_01_01_000004_create_cloud_table.php new file mode 100644 index 0000000..81d884b --- /dev/null +++ b/database/migrations/2024_01_01_000004_create_cloud_table.php @@ -0,0 +1,50 @@ +id(); + $table->string('name'); + $table->string('provider'); // aws, alibaba, tencent, gcp, azure, digitalocean, vultr, hetzner + $table->string('account_id')->nullable(); // AWS account ID, etc. + $table->text('credentials'); // encrypted JSON + $table->string('region')->nullable(); // default region + $table->json('regions')->nullable(); // all available regions + $table->boolean('is_active')->default(true); + $table->string('status')->default('unknown'); + $table->json('tags')->nullable(); + $table->timestamp('last_sync_at')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('cloud_resources', function (Blueprint $table) { + $table->id(); + $table->foreignId('account_id')->constrained('cloud_accounts')->cascadeOnDelete(); + $table->string('resource_id'); // cloud provider resource ID + $table->string('resource_type'); // ec2, ecs, rds, vpc, s3, etc. + $table->string('name'); + $table->string('status'); + $table->string('region'); + $table->json('properties')->nullable(); + $table->json('tags')->nullable(); + $table->decimal('monthly_cost', 10, 4)->nullable(); + $table->timestamps(); + + $table->unique(['account_id', 'resource_id', 'resource_type']); + }); + } + + public function down(): void + { + Schema::dropIfExists('cloud_resources'); + Schema::dropIfExists('cloud_accounts'); + } +}; diff --git a/database/migrations/2024_01_01_000005_create_security_table.php b/database/migrations/2024_01_01_000005_create_security_table.php new file mode 100644 index 0000000..771c09f --- /dev/null +++ b/database/migrations/2024_01_01_000005_create_security_table.php @@ -0,0 +1,64 @@ +id(); + $table->string('target_type'); // vm, container, host, cloud_resource + $table->unsignedBigInteger('target_id'); + $table->string('target_name'); + $table->string('scan_type'); // vulnerability, cis, pci, soc2, custom + $table->string('status'); // queued, running, completed, failed + $table->float('score')->nullable(); // 0-100 + $table->string('grade')->nullable(); // A, B, C, D, F + $table->integer('critical_count')->default(0); + $table->integer('high_count')->default(0); + $table->integer('medium_count')->default(0); + $table->integer('low_count')->default(0); + $table->integer('info_count')->default(0); + $table->json('findings')->nullable(); + $table->text('raw_output')->nullable(); + $table->foreignId('initiated_by')->constrained('users'); + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + }); + + Schema::create('security_scores', function (Blueprint $table) { + $table->id(); + $table->string('target_type'); + $table->unsignedBigInteger('target_id'); + $table->float('score')->default(0); + $table->string('grade')->default('F'); + $table->json('breakdown')->nullable(); + $table->timestamp('calculated_at'); + $table->timestamps(); + + $table->unique(['target_type', 'target_id']); + }); + + Schema::create('security_policies', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('type'); // firewall, password, access, encryption + $table->text('description')->nullable(); + $table->json('rules'); + $table->boolean('is_active')->default(true); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('security_policies'); + Schema::dropIfExists('security_scores'); + Schema::dropIfExists('security_scans'); + } +}; diff --git a/database/migrations/2024_01_01_000006_create_backups_table.php b/database/migrations/2024_01_01_000006_create_backups_table.php new file mode 100644 index 0000000..04c446b --- /dev/null +++ b/database/migrations/2024_01_01_000006_create_backups_table.php @@ -0,0 +1,69 @@ +id(); + $table->string('name'); + $table->string('target_type'); // vm, container, database, files + $table->json('target_ids'); + $table->string('schedule'); // cron expression + $table->string('retention_type')->default('count'); // count, days + $table->integer('retention_value')->default(7); + $table->string('storage_type'); // local, s3, sftp, nfs + $table->json('storage_config')->nullable(); // encrypted + $table->boolean('is_active')->default(true); + $table->json('compression')->nullable(); + $table->json('encryption')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + }); + + Schema::create('backups', function (Blueprint $table) { + $table->id(); + $table->foreignId('policy_id')->nullable()->constrained('backup_policies')->nullOnDelete(); + $table->string('target_type'); + $table->unsignedBigInteger('target_id'); + $table->string('target_name'); + $table->string('status'); // running, success, failed, expired + $table->string('storage_path')->nullable(); + $table->bigInteger('size_bytes')->nullable(); + $table->string('checksum')->nullable(); + $table->json('metadata')->nullable(); + $table->text('notes')->nullable(); + $table->timestamp('expires_at')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + }); + + Schema::create('restore_jobs', function (Blueprint $table) { + $table->id(); + $table->foreignId('backup_id')->constrained('backups'); + $table->string('target_type'); + $table->unsignedBigInteger('target_id')->nullable(); + $table->string('restore_mode'); // overwrite, new, test + $table->json('restore_config')->nullable(); + $table->string('status'); // queued, running, success, failed + $table->text('log')->nullable(); + $table->foreignId('initiated_by')->constrained('users'); + $table->timestamp('started_at')->nullable(); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('restore_jobs'); + Schema::dropIfExists('backups'); + Schema::dropIfExists('backup_policies'); + } +}; diff --git a/database/migrations/2024_01_01_000007_create_monitoring_table.php b/database/migrations/2024_01_01_000007_create_monitoring_table.php new file mode 100644 index 0000000..39669e3 --- /dev/null +++ b/database/migrations/2024_01_01_000007_create_monitoring_table.php @@ -0,0 +1,75 @@ +id(); + $table->string('target_type'); + $table->unsignedBigInteger('target_id'); + $table->string('metric_name'); + $table->float('value'); + $table->string('unit')->nullable(); + $table->json('labels')->nullable(); + $table->timestamp('recorded_at'); + $table->timestamps(); + + $table->index(['target_type', 'target_id', 'metric_name', 'recorded_at']); + }); + + Schema::create('alert_rules', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('target_type')->nullable(); // null = all targets + $table->string('metric_name'); + $table->string('condition'); // >, <, >=, <=, ==, != + $table->float('threshold'); + $table->integer('duration_seconds')->default(60); + $table->string('severity'); // critical, warning, info + $table->json('notification_channels')->nullable(); + $table->boolean('is_active')->default(true); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + }); + + Schema::create('alerts', function (Blueprint $table) { + $table->id(); + $table->foreignId('rule_id')->nullable()->constrained('alert_rules')->nullOnDelete(); + $table->string('target_type'); + $table->unsignedBigInteger('target_id'); + $table->string('target_name'); + $table->string('title'); + $table->text('message'); + $table->string('severity'); // critical, warning, info + $table->string('status')->default('firing'); // firing, acknowledged, resolved + $table->foreignId('acknowledged_by')->nullable()->constrained('users'); + $table->timestamp('acknowledged_at')->nullable(); + $table->timestamp('resolved_at')->nullable(); + $table->timestamps(); + }); + + Schema::create('monitoring_dashboards', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->text('description')->nullable(); + $table->json('panels'); // dashboard panel config + $table->boolean('is_default')->default(false); + $table->boolean('is_public')->default(false); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('monitoring_dashboards'); + Schema::dropIfExists('alerts'); + Schema::dropIfExists('alert_rules'); + Schema::dropIfExists('metrics'); + } +}; diff --git a/database/migrations/2024_01_01_000008_create_identity_table.php b/database/migrations/2024_01_01_000008_create_identity_table.php new file mode 100644 index 0000000..9a0bc7f --- /dev/null +++ b/database/migrations/2024_01_01_000008_create_identity_table.php @@ -0,0 +1,72 @@ +id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->string('title'); + $table->string('category')->nullable(); + $table->string('url')->nullable(); + $table->string('username')->nullable(); + $table->text('password'); // encrypted + $table->text('notes')->nullable(); // encrypted + $table->json('custom_fields')->nullable(); // encrypted + $table->json('tags')->nullable(); + $table->boolean('is_shared')->default(false); + $table->json('shared_with')->nullable(); // user IDs + $table->timestamp('expires_at')->nullable(); + $table->timestamp('last_used_at')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('certificates', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('domain'); + $table->json('san')->nullable(); // Subject Alternative Names + $table->text('certificate'); // encrypted + $table->text('private_key'); // encrypted + $table->text('chain')->nullable(); // encrypted + $table->string('issuer')->nullable(); + $table->string('type'); // self-signed, lets-encrypt, custom + $table->boolean('auto_renew')->default(false); + $table->timestamp('issued_at')->nullable(); + $table->timestamp('expires_at'); + $table->json('tags')->nullable(); + $table->string('status'); // valid, expiring, expired, revoked + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('ssh_keys', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id')->constrained('users')->cascadeOnDelete(); + $table->string('name'); + $table->string('fingerprint')->unique(); + $table->text('public_key'); + $table->text('private_key')->nullable(); // encrypted + $table->string('algorithm')->default('rsa'); + $table->integer('key_size')->default(4096); + $table->boolean('has_passphrase')->default(false); + $table->json('tags')->nullable(); + $table->timestamps(); + $table->softDeletes(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ssh_keys'); + Schema::dropIfExists('certificates'); + Schema::dropIfExists('password_entries'); + } +}; diff --git a/database/migrations/2024_01_01_000009_create_wiki_table.php b/database/migrations/2024_01_01_000009_create_wiki_table.php new file mode 100644 index 0000000..6374888 --- /dev/null +++ b/database/migrations/2024_01_01_000009_create_wiki_table.php @@ -0,0 +1,73 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->string('icon')->default('BookOpen'); + $table->string('color')->default('#3b82f6'); + $table->boolean('is_public')->default(false); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + }); + + Schema::create('wiki_pages', function (Blueprint $table) { + $table->id(); + $table->foreignId('space_id')->constrained('wiki_spaces')->cascadeOnDelete(); + $table->foreignId('parent_id')->nullable()->constrained('wiki_pages')->nullOnDelete(); + $table->string('title'); + $table->string('slug'); + $table->longText('content'); // Markdown content + $table->longText('content_html')->nullable(); // rendered HTML cache + $table->json('tags')->nullable(); + $table->boolean('is_published')->default(true); + $table->integer('order')->default(0); + $table->foreignId('created_by')->constrained('users'); + $table->foreignId('updated_by')->nullable()->constrained('users'); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['space_id', 'slug']); + }); + + Schema::create('wiki_page_revisions', function (Blueprint $table) { + $table->id(); + $table->foreignId('page_id')->constrained('wiki_pages')->cascadeOnDelete(); + $table->longText('content'); + $table->string('change_summary')->nullable(); + $table->foreignId('created_by')->constrained('users'); + $table->timestamps(); + }); + + Schema::create('wiki_attachments', function (Blueprint $table) { + $table->id(); + $table->foreignId('page_id')->nullable()->constrained('wiki_pages')->nullOnDelete(); + $table->string('filename'); + $table->string('original_name'); + $table->string('mime_type'); + $table->bigInteger('size'); + $table->string('path'); + $table->string('disk')->default('local'); + $table->foreignId('uploaded_by')->constrained('users'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('wiki_attachments'); + Schema::dropIfExists('wiki_page_revisions'); + Schema::dropIfExists('wiki_pages'); + Schema::dropIfExists('wiki_spaces'); + } +}; diff --git a/database/migrations/2024_01_01_000010_create_ldap_table.php b/database/migrations/2024_01_01_000010_create_ldap_table.php new file mode 100644 index 0000000..b2f95fb --- /dev/null +++ b/database/migrations/2024_01_01_000010_create_ldap_table.php @@ -0,0 +1,37 @@ +id(); + $table->string('name')->default('default'); + $table->string('host'); + $table->integer('port')->default(389); + $table->boolean('use_ssl')->default(false); + $table->boolean('use_tls')->default(false); + $table->string('base_dn'); + $table->string('bind_dn')->nullable(); + $table->text('bind_password')->nullable(); // encrypted + $table->string('user_search_base')->nullable(); + $table->string('user_filter')->default('(sAMAccountName={username})'); + $table->string('group_search_base')->nullable(); + $table->string('group_filter')->nullable(); + $table->string('admin_group_dn')->nullable(); + $table->json('attribute_map')->nullable(); // ldap attr -> user field map + $table->boolean('is_active')->default(false); + $table->string('status')->default('unconfigured'); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ldap_configs'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php new file mode 100644 index 0000000..7370ab2 --- /dev/null +++ b/database/seeders/DatabaseSeeder.php @@ -0,0 +1,96 @@ + 'super-admin']); + $admin = Role::firstOrCreate(['name' => 'admin']); + $operator = Role::firstOrCreate(['name' => 'operator']); + $viewer = Role::firstOrCreate(['name' => 'viewer']); + + // Create permissions + $permissions = [ + // Hypervisors + 'hypervisors.view', 'hypervisors.create', 'hypervisors.edit', 'hypervisors.delete', + 'vms.view', 'vms.power', 'vms.create', 'vms.delete', + 'templates.view', 'templates.create', 'templates.edit', 'templates.delete', 'templates.deploy', + // Containers + 'containers.view', 'containers.manage', + 'stacks.view', 'stacks.create', 'stacks.edit', 'stacks.delete', + // Cloud + 'cloud.view', 'cloud.create', 'cloud.edit', 'cloud.delete', + // Security + 'security.view', 'security.scan', + // Backups + 'backups.view', 'backups.create', 'backups.restore', 'backups.delete', + // Monitoring + 'monitoring.view', 'monitoring.alerts', + // Identity + 'identity.view', 'identity.create', 'identity.edit', 'identity.delete', + // Wiki + 'wiki.view', 'wiki.create', 'wiki.edit', 'wiki.delete', + // ACL + 'acl.view', 'acl.manage', + // Admin + 'admin.view', 'admin.manage', + ]; + + foreach ($permissions as $permission) { + Permission::firstOrCreate(['name' => $permission]); + } + + // Assign permissions to roles + $admin->syncPermissions(Permission::whereNotIn('name', ['admin.manage', 'acl.manage'])->get()); + $operator->syncPermissions([ + 'hypervisors.view', 'vms.view', 'vms.power', + 'containers.view', 'containers.manage', + 'stacks.view', 'cloud.view', + 'security.view', 'backups.view', + 'monitoring.view', 'monitoring.alerts', + 'identity.view', 'wiki.view', 'wiki.create', 'wiki.edit', + ]); + $viewer->syncPermissions([ + 'hypervisors.view', 'vms.view', 'containers.view', + 'stacks.view', 'cloud.view', 'security.view', + 'backups.view', 'monitoring.view', 'identity.view', 'wiki.view', + ]); + + // Create default super admin user + $user = User::firstOrCreate( + ['email' => 'admin@infra.local'], + [ + 'name' => 'Super Admin', + 'password' => Hash::make('Admin@123456'), + 'is_active' => true, + ] + ); + $user->assignRole('super-admin'); + + // Default settings + \App\Models\Setting::firstOrCreate(['key' => 'app.name'], [ + 'group' => 'general', + 'value' => 'Infra Manager', + 'type' => 'string', + ]); + \App\Models\Setting::firstOrCreate(['key' => 'app.theme'], [ + 'group' => 'general', + 'value' => 'dark', + 'type' => 'string', + ]); + \App\Models\Setting::firstOrCreate(['key' => 'security.session_timeout'], [ + 'group' => 'security', + 'value' => '480', + 'type' => 'integer', + ]); + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..66b84f7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,119 @@ +version: '3.9' + +services: + # ─── Nginx Web Server ─────────────────────────────────────────────── + nginx: + image: nginx:1.27-alpine + container_name: infra-nginx + restart: unless-stopped + ports: + - "${APP_PORT:-8080}:80" + - "${APP_HTTPS_PORT:-8443}:443" + volumes: + - .:/var/www/html:ro + - ./docker/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro + - ./docker/nginx/ssl:/etc/nginx/ssl:ro + - nginx_logs:/var/log/nginx + depends_on: + - php + networks: + - infra_network + + # ─── PHP-FPM Application ──────────────────────────────────────────── + php: + build: + context: . + dockerfile: docker/php/Dockerfile + args: + USER_ID: ${USER_ID:-1000} + GROUP_ID: ${GROUP_ID:-1000} + container_name: infra-php + restart: unless-stopped + volumes: + - .:/var/www/html + - ./storage:/var/www/html/storage + - ./database:/var/www/html/database + - php_logs:/var/log/supervisor + environment: + - APP_ENV=${APP_ENV:-local} + - APP_DEBUG=${APP_DEBUG:-true} + depends_on: + redis: + condition: service_healthy + networks: + - infra_network + + # ─── Redis Cache & Queue ───────────────────────────────────────────── + redis: + image: redis:7.4-alpine + container_name: infra-redis + restart: unless-stopped + command: redis-server --requirepass ${REDIS_PASSWORD:-infra_redis_secret} --maxmemory 256mb --maxmemory-policy allkeys-lru + ports: + - "${REDIS_PORT:-6379}:6379" + volumes: + - redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-infra_redis_secret}", "ping"] + interval: 5s + timeout: 3s + retries: 5 + networks: + - infra_network + + # ─── Vite Dev Server (dev only) ────────────────────────────────────── + vite: + image: node:22-alpine + container_name: infra-vite + restart: unless-stopped + working_dir: /var/www/html + command: sh -c "npm install && npm run dev" + volumes: + - .:/var/www/html + ports: + - "5173:5173" + networks: + - infra_network + profiles: + - dev + + # ─── MySQL (optional - for production upgrade) ──────────────────────── + mysql: + image: mysql:8.0 + container_name: infra-mysql + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-root_secret} + MYSQL_DATABASE: ${MYSQL_DATABASE:-infra_manager} + MYSQL_USER: ${MYSQL_USER:-infra_user} + MYSQL_PASSWORD: ${MYSQL_PASSWORD:-infra_secret} + ports: + - "${MYSQL_PORT:-3306}:3306" + volumes: + - mysql_data:/var/lib/mysql + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD:-root_secret}"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - infra_network + profiles: + - mysql + +volumes: + redis_data: + driver: local + mysql_data: + driver: local + nginx_logs: + driver: local + php_logs: + driver: local + +networks: + infra_network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 diff --git a/docker/nginx/default.conf b/docker/nginx/default.conf new file mode 100644 index 0000000..06ec006 --- /dev/null +++ b/docker/nginx/default.conf @@ -0,0 +1,97 @@ +server { + listen 80; + listen [::]:80; + server_name _; + root /var/www/html/public; + index index.php; + + charset utf-8; + client_max_body_size 100M; + + # Security headers + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header X-XSS-Protection "1; mode=block"; + add_header Referrer-Policy "strict-origin-when-cross-origin"; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 300; + fastcgi_buffer_size 128k; + fastcgi_buffers 4 256k; + } + + location ~ /\.(?!well-known).* { + deny all; + } + + # WebSocket support for Vite HMR in dev + location /vite-hmr { + proxy_pass http://vite:5173; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + } + + # Gzip compression + gzip on; + gzip_vary on; + gzip_min_length 1000; + gzip_proxied expired no-cache no-store private auth; + gzip_types text/plain text/css text/xml application/json application/javascript application/xml+rss application/atom+xml image/svg+xml; + + # Access log + access_log /var/log/nginx/infra-manager-access.log; + error_log /var/log/nginx/infra-manager-error.log; +} + +server { + listen 443 ssl; + listen [::]:443 ssl; + server_name _; + root /var/www/html/public; + index index.php; + + ssl_certificate /etc/nginx/ssl/cert.pem; + ssl_certificate_key /etc/nginx/ssl/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + charset utf-8; + client_max_body_size 100M; + + add_header X-Frame-Options "SAMEORIGIN"; + add_header X-Content-Type-Options "nosniff"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains"; + + location / { + try_files $uri $uri/ /index.php?$query_string; + } + + location = /favicon.ico { access_log off; log_not_found off; } + location = /robots.txt { access_log off; log_not_found off; } + + error_page 404 /index.php; + + location ~ \.php$ { + fastcgi_pass php:9000; + fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; + include fastcgi_params; + fastcgi_read_timeout 300; + } + + location ~ /\.(?!well-known).* { + deny all; + } +} diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 0000000..91ed5c4 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,92 @@ +FROM php:8.3-fpm-alpine + +LABEL maintainer="Infra Manager" + +# Build arguments +ARG NODE_VERSION=22 +ARG USER_ID=1000 +ARG GROUP_ID=1000 + +# Install system dependencies +RUN apk add --no-cache \ + bash \ + curl \ + git \ + icu-dev \ + libzip-dev \ + linux-headers \ + oniguruma-dev \ + openssl-dev \ + sqlite \ + sqlite-dev \ + supervisor \ + unzip \ + zip \ + libpng-dev \ + libjpeg-turbo-dev \ + freetype-dev \ + shadow \ + nodejs \ + npm + +# Install PHP extensions +RUN docker-php-ext-configure gd --with-freetype --with-jpeg \ + && docker-php-ext-install \ + bcmath \ + exif \ + gd \ + intl \ + mbstring \ + opcache \ + pcntl \ + pdo \ + pdo_sqlite \ + zip + +# Install pdo_mysql for later MySQL migration +RUN apk add --no-cache mysql-client \ + && docker-php-ext-install pdo_mysql + +# Install Redis extension +RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \ + && pecl install redis \ + && docker-php-ext-enable redis \ + && apk del .build-deps + +# Install Composer +COPY --from=composer:2.7 /usr/bin/composer /usr/bin/composer + +# Configure opcache +RUN docker-php-ext-enable opcache +COPY docker/php/opcache.ini /usr/local/etc/php/conf.d/opcache.ini + +# PHP configuration +COPY docker/php/php.ini /usr/local/etc/php/conf.d/custom.ini + +# Create application user +RUN addgroup -g ${GROUP_ID} appgroup 2>/dev/null || true \ + && adduser -u ${USER_ID} -G appgroup -s /bin/bash -D appuser 2>/dev/null || true + +# Set working directory +WORKDIR /var/www/html + +# Copy application +COPY --chown=appuser:appgroup . . + +# Install PHP dependencies +RUN composer install --no-dev --optimize-autoloader --no-interaction 2>/dev/null || \ + composer install --optimize-autoloader --no-interaction + +# Install Node dependencies and build assets +RUN npm ci && npm run build + +# Set permissions +RUN chown -R appuser:appgroup /var/www/html \ + && chmod -R 755 /var/www/html/storage \ + && chmod -R 755 /var/www/html/bootstrap/cache + +USER appuser + +EXPOSE 9000 + +CMD ["php-fpm"] diff --git a/docker/php/opcache.ini b/docker/php/opcache.ini new file mode 100644 index 0000000..801d6e4 --- /dev/null +++ b/docker/php/opcache.ini @@ -0,0 +1,8 @@ +opcache.enable=1 +opcache.memory_consumption=256 +opcache.interned_strings_buffer=16 +opcache.max_accelerated_files=20000 +opcache.revalidate_freq=0 +opcache.validate_timestamps=0 +opcache.save_comments=1 +opcache.fast_shutdown=0 diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 0000000..aa4874e --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,7 @@ +upload_max_filesize = 100M +post_max_size = 100M +memory_limit = 512M +max_execution_time = 300 +max_input_vars = 5000 +date.timezone = UTC +expose_php = Off diff --git a/docker/supervisor/supervisord.conf b/docker/supervisor/supervisord.conf new file mode 100644 index 0000000..aa8bb14 --- /dev/null +++ b/docker/supervisor/supervisord.conf @@ -0,0 +1,32 @@ +[supervisord] +nodaemon=true +user=root +logfile=/var/log/supervisor/supervisord.log +pidfile=/var/run/supervisord.pid + +[program:php-fpm] +command=php-fpm +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=true + +[program:laravel-worker] +process_name=%(program_name)s_%(process_num)02d +command=php /var/www/html/artisan queue:work --sleep=3 --tries=3 --max-time=3600 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +numprocs=2 +redirect_stderr=true +stdout_logfile=/var/log/supervisor/worker.log +stopwaitsecs=3600 + +[program:laravel-scheduler] +command=bash -c "while [ true ]; do (php /var/www/html/artisan schedule:run --verbose --no-interaction &); sleep 60; done" +autostart=true +autorestart=true +redirect_stderr=true +stdout_logfile=/var/log/supervisor/scheduler.log diff --git a/package.json b/package.json new file mode 100644 index 0000000..99519ca --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "infra-manager", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build" + }, + "dependencies": { + "@headlessui/react": "^2.2.0", + "@heroicons/react": "^2.2.0", + "@inertiajs/react": "^2.0.3", + "@radix-ui/react-dialog": "^1.1.4", + "@radix-ui/react-dropdown-menu": "^2.1.4", + "@radix-ui/react-progress": "^1.1.1", + "@radix-ui/react-scroll-area": "^1.2.2", + "@radix-ui/react-select": "^2.1.4", + "@radix-ui/react-separator": "^1.1.1", + "@radix-ui/react-slot": "^1.1.1", + "@radix-ui/react-tabs": "^1.1.2", + "@radix-ui/react-toast": "^1.2.4", + "@radix-ui/react-tooltip": "^1.1.6", + "axios": "^1.7.4", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.0.4", + "framer-motion": "^11.18.0", + "lucide-react": "^0.469.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router-dom": "^7.1.2", + "recharts": "^2.15.0", + "tailwind-merge": "^2.6.0", + "tailwindcss-animate": "^1.0.7", + "xterm": "^5.3.0", + "xterm-addon-fit": "^0.8.0", + "zustand": "^5.0.3" + }, + "devDependencies": { + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.15", + "@types/node": "^22.10.7", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "autoprefixer": "^10.4.20", + "postcss": "^8.5.1", + "tailwindcss": "^3.4.17", + "vite": "^6.0.11" + } +} diff --git a/resources/css/app.css b/resources/css/app.css new file mode 100644 index 0000000..cde7ceb --- /dev/null +++ b/resources/css/app.css @@ -0,0 +1,112 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + --primary: 184 100% 50%; + --primary-foreground: 222.2 47.4% 11.2%; + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 184 100% 50%; + --radius: 0.5rem; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + font-feature-settings: "rlig" 1, "calt" 1; + } +} + +@layer components { + /* Aceternity-style glassmorphism card */ + .glass-card { + @apply relative overflow-hidden rounded-xl border border-white/10 bg-white/5 backdrop-blur-xl; + } + + /* Gradient border effect */ + .gradient-border { + @apply relative; + background: linear-gradient(var(--background), var(--background)) padding-box, + linear-gradient(135deg, #00d4ff, #9d4edd, #00ff88) border-box; + border: 1px solid transparent; + } + + /* Status badge variants */ + .status-online { @apply bg-green-500/20 text-green-400 border border-green-500/30; } + .status-offline { @apply bg-red-500/20 text-red-400 border border-red-500/30; } + .status-warning { @apply bg-yellow-500/20 text-yellow-400 border border-yellow-500/30; } + .status-unknown { @apply bg-gray-500/20 text-gray-400 border border-gray-500/30; } + .status-running { @apply bg-cyan-500/20 text-cyan-400 border border-cyan-500/30; } + .status-stopped { @apply bg-slate-500/20 text-slate-400 border border-slate-500/30; } + + /* Neon glow effects */ + .glow-cyan { box-shadow: 0 0 20px rgba(0, 212, 255, 0.3); } + .glow-green { box-shadow: 0 0 20px rgba(0, 255, 136, 0.3); } + .glow-purple { box-shadow: 0 0 20px rgba(157, 78, 221, 0.3); } + + /* Custom scrollbar */ + .custom-scroll::-webkit-scrollbar { + @apply w-1.5; + } + .custom-scroll::-webkit-scrollbar-track { + @apply bg-transparent; + } + .custom-scroll::-webkit-scrollbar-thumb { + @apply rounded-full bg-white/10; + } + .custom-scroll::-webkit-scrollbar-thumb:hover { + @apply bg-white/20; + } + + /* Terminal style */ + .terminal { + @apply font-mono text-sm bg-black/60 text-green-400 p-4 rounded-lg; + } +} + +/* Grid background pattern */ +.bg-grid-pattern { + background-image: + linear-gradient(rgba(255,255,255,.03) 1px, transparent 1px), + linear-gradient(90deg, rgba(255,255,255,.03) 1px, transparent 1px); + background-size: 50px 50px; +} + +/* Aurora background */ +.aurora-bg { + background: linear-gradient( + 125deg, + rgba(0, 212, 255, 0.05) 0%, + rgba(157, 78, 221, 0.05) 33%, + rgba(0, 255, 136, 0.05) 66%, + rgba(0, 212, 255, 0.05) 100% + ); + background-size: 400% 400%; + animation: aurora 15s ease infinite; +} + +@keyframes aurora { + 0% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + 100% { background-position: 0% 50%; } +} diff --git a/resources/js/Components/ui/Button.jsx b/resources/js/Components/ui/Button.jsx new file mode 100644 index 0000000..f604b2c --- /dev/null +++ b/resources/js/Components/ui/Button.jsx @@ -0,0 +1,42 @@ +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; + +export function Button({ children, variant = 'primary', size = 'md', className, loading, icon: Icon, ...props }) { + const variants = { + primary: 'bg-cyan-500 hover:bg-cyan-400 text-black font-semibold shadow-[0_0_20px_rgba(0,212,255,0.3)] hover:shadow-[0_0_30px_rgba(0,212,255,0.5)]', + secondary: 'bg-white/10 hover:bg-white/15 text-white border border-white/20', + danger: 'bg-red-500/20 hover:bg-red-500/30 text-red-400 border border-red-500/30', + ghost: 'hover:bg-white/5 text-white/70 hover:text-white', + outline: 'border border-cyan-500/50 text-cyan-400 hover:bg-cyan-500/10', + }; + const sizes = { + sm: 'h-7 px-3 text-xs gap-1.5', + md: 'h-9 px-4 text-sm gap-2', + lg: 'h-11 px-6 text-base gap-2', + icon: 'h-9 w-9 p-0', + }; + + return ( + + {loading ? ( + + + + + ) : Icon ? ( + + ) : null} + {size !== 'icon' && children} + + ); +} diff --git a/resources/js/Components/ui/DataTable.jsx b/resources/js/Components/ui/DataTable.jsx new file mode 100644 index 0000000..73845e2 --- /dev/null +++ b/resources/js/Components/ui/DataTable.jsx @@ -0,0 +1,137 @@ +import { useState } from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { ChevronUp, ChevronDown, Search, ChevronsLeft, ChevronsRight, ChevronLeft, ChevronRight } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export function DataTable({ columns, data, onRowClick, emptyMessage = 'No data found', searchable = true, className }) { + const [search, setSearch] = useState(''); + const [sortCol, setSortCol] = useState(null); + const [sortDir, setSortDir] = useState('asc'); + const [page, setPage] = useState(1); + const perPage = 15; + + const filtered = search + ? data.filter(row => columns.some(col => { + const val = col.accessor ? row[col.accessor] : ''; + return String(val ?? '').toLowerCase().includes(search.toLowerCase()); + })) + : data; + + const sorted = sortCol + ? [...filtered].sort((a, b) => { + const va = a[sortCol] ?? ''; + const vb = b[sortCol] ?? ''; + const cmp = String(va).localeCompare(String(vb), undefined, { numeric: true }); + return sortDir === 'asc' ? cmp : -cmp; + }) + : filtered; + + const totalPages = Math.ceil(sorted.length / perPage); + const paginated = sorted.slice((page - 1) * perPage, page * perPage); + + const handleSort = (accessor) => { + if (sortCol === accessor) { + setSortDir(d => d === 'asc' ? 'desc' : 'asc'); + } else { + setSortCol(accessor); + setSortDir('asc'); + } + setPage(1); + }; + + return ( +
+ {searchable && ( +
+ + { setSearch(e.target.value); setPage(1); }} + className="w-full pl-9 pr-4 py-2 bg-white/5 border border-white/10 rounded-lg text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-cyan-500/50 focus:bg-white/[0.07] transition-all" + /> +
+ )} + +
+ + + + {columns.map(col => ( + + ))} + + + + + {paginated.length === 0 ? ( + + + + ) : ( + paginated.map((row, i) => ( + onRowClick?.(row)} + className={cn( + 'border-b border-white/[0.05] last:border-0 transition-colors', + onRowClick && 'cursor-pointer hover:bg-white/[0.03]' + )} + > + {columns.map(col => ( + + ))} + + )) + )} + + +
col.accessor && handleSort(col.accessor)} + > +
+ {col.header} + {col.accessor && sortCol === col.accessor && ( + sortDir === 'asc' + ? + : + )} +
+
+ {emptyMessage} +
+ {col.render ? col.render(row) : row[col.accessor]} +
+
+ + {totalPages > 1 && ( +
+ {filtered.length} results +
+ + + Page {page} of {totalPages} + + +
+
+ )} +
+ ); +} diff --git a/resources/js/Components/ui/GlassCard.jsx b/resources/js/Components/ui/GlassCard.jsx new file mode 100644 index 0000000..edac4b6 --- /dev/null +++ b/resources/js/Components/ui/GlassCard.jsx @@ -0,0 +1,59 @@ +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; + +export function GlassCard({ children, className, hover = true, glow, ...props }) { + return ( + + {/* Inner gradient */} +
+
{children}
+ + ); +} + +export function StatCard({ title, value, subtitle, icon: Icon, color = 'cyan', trend, className }) { + const colors = { + cyan: { text: 'text-cyan-400', bg: 'bg-cyan-500/10', border: 'border-cyan-500/20' }, + green: { text: 'text-green-400', bg: 'bg-green-500/10', border: 'border-green-500/20' }, + purple: { text: 'text-purple-400', bg: 'bg-purple-500/10', border: 'border-purple-500/20' }, + orange: { text: 'text-orange-400', bg: 'bg-orange-500/10', border: 'border-orange-500/20' }, + red: { text: 'text-red-400', bg: 'bg-red-500/10', border: 'border-red-500/20' }, + blue: { text: 'text-blue-400', bg: 'bg-blue-500/10', border: 'border-blue-500/20' }, + }; + const c = colors[color] ?? colors.cyan; + + return ( + +
+
+

{title}

+

{value}

+ {subtitle &&

{subtitle}

} + {trend !== undefined && ( +

= 0 ? 'text-green-400' : 'text-red-400')}> + {trend >= 0 ? '↑' : '↓'} {Math.abs(trend)}% vs last hour +

+ )} +
+ {Icon && ( +
+ +
+ )} +
+
+ ); +} diff --git a/resources/js/Components/ui/Modal.jsx b/resources/js/Components/ui/Modal.jsx new file mode 100644 index 0000000..7ec4d8f --- /dev/null +++ b/resources/js/Components/ui/Modal.jsx @@ -0,0 +1,54 @@ +import { Fragment } from 'react'; +import { Dialog, Transition } from '@headlessui/react'; +import { X } from 'lucide-react'; +import { cn } from '@/lib/utils'; + +export function Modal({ isOpen, onClose, title, children, size = 'md', className }) { + const sizes = { + sm: 'max-w-md', + md: 'max-w-2xl', + lg: 'max-w-4xl', + xl: 'max-w-6xl', + full: 'max-w-[95vw]', + }; + + return ( + + + +
+ + +
+
+ + + {title && ( +
+ {title} + +
+ )} +
{children}
+
+
+
+
+
+
+ ); +} diff --git a/resources/js/Components/ui/ProgressBar.jsx b/resources/js/Components/ui/ProgressBar.jsx new file mode 100644 index 0000000..9b3bdf9 --- /dev/null +++ b/resources/js/Components/ui/ProgressBar.jsx @@ -0,0 +1,40 @@ +import { motion } from 'framer-motion'; +import { cn } from '@/lib/utils'; + +export function ProgressBar({ value, max = 100, className, showLabel = true, color, size = 'md' }) { + const pct = Math.min(100, Math.max(0, (value / max) * 100)); + + const autoColor = color ?? ( + pct >= 90 ? 'red' : + pct >= 70 ? 'yellow' : + pct >= 50 ? 'cyan' : 'green' + ); + + const colors = { + cyan: { bar: 'bg-cyan-400', glow: 'shadow-[0_0_10px_rgba(0,212,255,0.5)]' }, + green: { bar: 'bg-green-400', glow: 'shadow-[0_0_10px_rgba(0,255,136,0.5)]' }, + yellow: { bar: 'bg-yellow-400', glow: 'shadow-[0_0_10px_rgba(255,255,0,0.5)]' }, + red: { bar: 'bg-red-400', glow: 'shadow-[0_0_10px_rgba(255,45,85,0.5)]' }, + purple: { bar: 'bg-purple-400', glow: 'shadow-[0_0_10px_rgba(157,78,221,0.5)]' }, + }; + const c = colors[autoColor] ?? colors.cyan; + const heights = { sm: 'h-1', md: 'h-1.5', lg: 'h-2' }; + + return ( +
+ {showLabel && ( +
+ {Math.round(pct)}% +
+ )} +
+ +
+
+ ); +} diff --git a/resources/js/Components/ui/StatusBadge.jsx b/resources/js/Components/ui/StatusBadge.jsx new file mode 100644 index 0000000..ec27511 --- /dev/null +++ b/resources/js/Components/ui/StatusBadge.jsx @@ -0,0 +1,34 @@ +import { cn, getStatusDot } from '@/lib/utils'; + +export function StatusBadge({ status, className }) { + const variants = { + online: 'bg-green-500/15 text-green-400 border-green-500/30', + running: 'bg-cyan-500/15 text-cyan-400 border-cyan-500/30', + active: 'bg-green-500/15 text-green-400 border-green-500/30', + stopped: 'bg-slate-500/15 text-slate-400 border-slate-500/30', + offline: 'bg-red-500/15 text-red-400 border-red-500/30', + error: 'bg-red-500/15 text-red-400 border-red-500/30', + warning: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30', + unknown: 'bg-gray-500/15 text-gray-400 border-gray-500/30', + paused: 'bg-yellow-500/15 text-yellow-400 border-yellow-500/30', + exited: 'bg-slate-500/15 text-slate-400 border-slate-500/30', + success: 'bg-green-500/15 text-green-400 border-green-500/30', + failed: 'bg-red-500/15 text-red-400 border-red-500/30', + pending: 'bg-blue-500/15 text-blue-400 border-blue-500/30', + queued: 'bg-purple-500/15 text-purple-400 border-purple-500/30', + }; + + const variant = variants[status?.toLowerCase()] ?? variants.unknown; + const dot = getStatusDot(status); + + return ( + + + {status} + + ); +} diff --git a/resources/js/Layouts/AppLayout.jsx b/resources/js/Layouts/AppLayout.jsx new file mode 100644 index 0000000..9ccab26 --- /dev/null +++ b/resources/js/Layouts/AppLayout.jsx @@ -0,0 +1,253 @@ +import { useState, useEffect } from 'react'; +import { Link, usePage } from '@inertiajs/react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { + LayoutDashboard, Server, Monitor, Layers, Box, Boxes, Cloud, + Shield, HardDrive, Activity, KeyRound, BookOpen, + Users, Settings, ChevronLeft, ChevronRight, Bell, Search, + LogOut, User, Menu, X, Zap, ChevronDown +} from 'lucide-react'; +import { cn, getStatusDot } from '@/lib/utils'; +import { Button } from '@/Components/ui/Button'; + +const ICONS = { + LayoutDashboard, Server, Monitor, Layers, Box, Boxes, Cloud, + Shield, HardDrive, Activity, KeyRound, BookOpen, Users, Settings +}; + +function NavItem({ item, collapsed, active }) { + const Icon = ICONS[item.icon] ?? Server; + + return ( + + {active && ( + + )} + + {!collapsed && ( + {item.name} + )} + {collapsed && ( +
+ {item.name} +
+ )} + + ); +} + +export default function AppLayout({ children, title }) { + const { auth, navigation, flash, app } = usePage().props; + const [collapsed, setCollapsed] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + const [notifOpen, setNotifOpen] = useState(false); + const currentPath = window.location.pathname; + + useEffect(() => { + setMobileOpen(false); + }, [currentPath]); + + const isActive = (href) => { + if (href === '/') return currentPath === '/'; + return currentPath.startsWith(href); + }; + + const topNav = navigation?.filter(n => !['acl', 'admin'].includes(n.key)) ?? []; + const adminNav = navigation?.filter(n => ['acl', 'admin'].includes(n.key)) ?? []; + + return ( +
+ {/* ── Sidebar ─────────────────────────────────────────── */} + + {/* Logo */} +
+
+ +
+ {!collapsed && ( + +

{app?.name ?? 'Infra Manager'}

+

v{app?.version ?? '1.0.0'}

+
+ )} +
+ + {/* Nav */} + + + {/* Collapse toggle */} + + + {/* User */} +
+ + {auth?.user?.name} + {!collapsed && ( +
+

{auth?.user?.name}

+

{auth?.user?.roles?.[0]}

+
+ )} + +
+ + +
+
+
+ + {/* ── Mobile Sidebar ──────────────────────────────────── */} + + {mobileOpen && ( + <> + setMobileOpen(false)} + /> + +
+
+
+ +
+

{app?.name ?? 'Infra Manager'}

+
+ +
+ +
+ + )} +
+ + {/* ── Main content ────────────────────────────────────── */} +
+ {/* Top Bar */} +
+ + + {title && ( +

{title}

+ )} + +
+ + {/* Notifications */} + + + {/* App env badge */} + {app?.env !== 'production' && ( + + {app?.env?.toUpperCase()} + + )} +
+ + {/* Flash messages */} + + {(flash?.success || flash?.error || flash?.warning) && ( + + {flash?.success || flash?.error || flash?.warning} + + )} + + + {/* Page Content */} +
+
+ {children} +
+
+
+
+ ); +} diff --git a/resources/js/Pages/Acl/Index.jsx b/resources/js/Pages/Acl/Index.jsx new file mode 100644 index 0000000..95fde41 --- /dev/null +++ b/resources/js/Pages/Acl/Index.jsx @@ -0,0 +1,120 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { Users, Shield, Plus, Edit, Trash2, UserCheck } from 'lucide-react'; +import { GlassCard, StatCard } from '@/Components/ui/GlassCard'; +import { Button } from '@/Components/ui/Button'; +import { DataTable } from '@/Components/ui/DataTable'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { timeAgo } from '@/lib/utils'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs'; +import { cn } from '@/lib/utils'; +import { useState } from 'react'; + +export default function AclIndex({ users, roles }) { + const [tab, setTab] = useState('users'); + + const userColumns = [ + { header: 'User', accessor: 'name', render: row => ( +
+ {row.name} +
+

{row.name}

+

{row.email}

+
+
+ )}, + { header: 'Auth', accessor: 'auth_source', render: row => ( + {row.auth_source} + )}, + { header: 'Roles', key: 'roles', render: row => ( +
+ {(row.roles ?? []).map(r => ( + {r} + ))} +
+ )}, + { header: 'Status', accessor: 'is_active', render: row => ( + + )}, + { header: 'Last Login', accessor: 'last_login_at', render: row => ( + {timeAgo(row.last_login_at)} + )}, + { header: '', key: 'actions', render: row => ( +
e.stopPropagation()}> +
+ )}, + ]; + + const roleColumns = [ + { header: 'Role', accessor: 'name', render: row => ( +
+

{row.name}

+

{row.permissions_count ?? 0} permissions

+
+ )}, + { header: 'Users', key: 'users', render: row => ( + {row.users_count ?? 0} + )}, + { header: '', key: 'actions', render: row => ( +
+
+ )}, + ]; + + return ( + + +
+
+
+

Access Control

+

Users, roles, and permissions management

+
+
+ {tab === 'users' && } + {tab === 'roles' && } +
+
+ +
+ + u.is_active).length ?? 0} icon={UserCheck} color="green" /> + u.auth_source === 'ldap').length ?? 0} icon={Shield} color="purple" /> + +
+ + + + {[{ value: 'users', label: 'Users' }, { value: 'roles', label: 'Roles & Permissions' }].map(t => ( + {t.label} + ))} + + + + + router.visit(`/acl/users/${row.id}/edit`)} + emptyMessage="No users found." /> + + + + + + + + + +
+
+ ); +} diff --git a/resources/js/Pages/Admin/Index.jsx b/resources/js/Pages/Admin/Index.jsx new file mode 100644 index 0000000..fe921c9 --- /dev/null +++ b/resources/js/Pages/Admin/Index.jsx @@ -0,0 +1,93 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { Settings, Users, Activity, Shield, Database, Globe, Server, ChevronRight } from 'lucide-react'; +import { GlassCard } from '@/Components/ui/GlassCard'; +import { Button } from '@/Components/ui/Button'; +import { motion } from 'framer-motion'; +import { timeAgo } from '@/lib/utils'; + +const adminSections = [ + { title: 'General Settings', href: '/admin/settings', icon: Settings, desc: 'App name, theme, session timeout' }, + { title: 'LDAP / SSO', href: '/admin/ldap', icon: Globe, desc: 'Configure LDAP/AD authentication' }, + { title: 'Users', href: '/acl/users', icon: Users, desc: 'Manage user accounts' }, + { title: 'Roles & ACL', href: '/acl/roles', icon: Shield, desc: 'Roles and permission management' }, + { title: 'Audit Log', href: '/admin/audit-log', icon: Activity, desc: 'User activity and system events' }, + { title: 'System Health', href: '/admin/system-health',icon: Server, desc: 'Database, queues, storage status' }, +]; + +export default function AdminIndex({ stats, recent_activity }) { + return ( + + +
+
+

Administration

+

System settings, users, and maintenance

+
+ +
+ {adminSections.map(s => { + const Icon = s.icon; + return ( + + +
+
+
+ +
+
+

{s.title}

+

{s.desc}

+
+
+ +
+
+
+ ); + })} +
+ + {/* System stats */} +
+ {[ + { label: 'Total Users', value: stats?.users ?? 0 }, + { label: 'Active Sessions', value: stats?.sessions ?? 0 }, + { label: 'Queue Jobs', value: stats?.queued ?? 0 }, + { label: 'DB Size', value: stats?.db_size ?? '—' }, + ].map(s => ( + +

{s.value}

+

{s.label}

+
+ ))} +
+ + {/* Recent activity */} + +
+

Recent Activity

+ +
+
+ {(recent_activity ?? []).map((log, i) => ( +
+
+ {log.causer?.name?.charAt(0)?.toUpperCase() ?? '?'} +
+
+

{log.description}

+

{log.causer?.name ?? 'System'} · {timeAgo(log.created_at)}

+
+
+ ))} + {(!recent_activity || recent_activity.length === 0) && ( +

No recent activity

+ )} +
+
+
+
+ ); +} diff --git a/resources/js/Pages/Admin/Ldap.jsx b/resources/js/Pages/Admin/Ldap.jsx new file mode 100644 index 0000000..16c8e48 --- /dev/null +++ b/resources/js/Pages/Admin/Ldap.jsx @@ -0,0 +1,146 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, useForm, router } from '@inertiajs/react'; +import { useState } from 'react'; +import { Globe, CheckCircle, XCircle, RefreshCw } from 'lucide-react'; +import { GlassCard } from '@/Components/ui/GlassCard'; +import { Button } from '@/Components/ui/Button'; +import { cn } from '@/lib/utils'; +import { motion } from 'framer-motion'; + +function Input({ className, ...props }) { + return ; +} +function Field({ label, desc, children }) { + return ( +
+
+

{label}

+ {desc &&

{desc}

} +
+
{children}
+
+ ); +} + +export default function LdapConfig({ config }) { + const { data, setData, put, processing } = useForm({ + host: config?.host ?? '', + port: config?.port ?? 389, + use_ssl: config?.use_ssl ?? false, + use_tls: config?.use_tls ?? false, + base_dn: config?.base_dn ?? '', + bind_dn: config?.bind_dn ?? '', + bind_password: '', + user_search_base: config?.user_search_base ?? '', + user_filter: config?.user_filter ?? '(sAMAccountName={username})', + admin_group_dn: config?.admin_group_dn ?? '', + is_active: config?.is_active ?? false, + }); + const [testResult, setTestResult] = useState(null); + const [testing, setTesting] = useState(false); + const [syncing, setSyncing] = useState(false); + + const test = async () => { + setTesting(true); + setTestResult(null); + try { + const res = await fetch('/admin/ldap/test', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content }, + body: JSON.stringify(data), + }); + setTestResult(await res.json()); + } catch (e) { + setTestResult({ success: false, message: e.message }); + } finally { + setTesting(false); + } + }; + + const sync = () => { + setSyncing(true); + router.post('/admin/ldap/sync', {}, { onFinish: () => setSyncing(false) }); + }; + + return ( + + +
+
+
+

LDAP / Active Directory

+

Integrate with your directory service for SSO

+
+
+ +
+
+ +
{ e.preventDefault(); put('/admin/ldap'); }}> +
+ +

Connection

+ + + + + setData('host', e.target.value)} placeholder="ldap.company.com" /> + + + setData('port', parseInt(e.target.value))} /> + + +
+ {[['use_ssl', 'LDAPS (SSL)'], ['use_tls', 'STARTTLS']].map(([k, l]) => ( + + ))} +
+
+
+ + +

Bind / Authentication

+ setData('base_dn', e.target.value)} placeholder="DC=company,DC=com" /> + setData('bind_dn', e.target.value)} placeholder="CN=ldap-reader,CN=Users,DC=company,DC=com" /> + setData('bind_password', e.target.value)} placeholder="Leave blank to keep existing" /> +
+ + +

User Search

+ setData('user_search_base', e.target.value)} placeholder="CN=Users,DC=company,DC=com" /> + + setData('user_filter', e.target.value)} /> + + + setData('admin_group_dn', e.target.value)} placeholder="CN=Infra-Admins,CN=Groups,..." /> + +
+ + {testResult && ( + + {testResult.success ? : } + {testResult.message} + + )} + +
+ + +
+
+
+
+
+ ); +} diff --git a/resources/js/Pages/Admin/Settings.jsx b/resources/js/Pages/Admin/Settings.jsx new file mode 100644 index 0000000..288f3e9 --- /dev/null +++ b/resources/js/Pages/Admin/Settings.jsx @@ -0,0 +1,115 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, useForm } from '@inertiajs/react'; +import { GlassCard } from '@/Components/ui/GlassCard'; +import { Button } from '@/Components/ui/Button'; +import { cn } from '@/lib/utils'; + +function Field({ label, desc, error, children }) { + return ( +
+
+

{label}

+ {desc &&

{desc}

} + {error &&

{error}

} +
+
{children}
+
+ ); +} + +function Input({ className, ...props }) { + return ( + + ); +} + +function Select({ className, children, ...props }) { + return ( + + ); +} + +export default function AdminSettings({ settings }) { + const { data, setData, put, processing, errors } = useForm({ + 'app.name': settings?.['app.name'] ?? 'Infra Manager', + 'app.theme': settings?.['app.theme'] ?? 'dark', + 'security.session_timeout': settings?.['security.session_timeout'] ?? 480, + 'security.mfa_required': settings?.['security.mfa_required'] ?? false, + 'mail.driver': settings?.['mail.driver'] ?? 'log', + 'mail.from': settings?.['mail.from'] ?? '', + }); + + return ( + + +
+
+

General Settings

+

Application-wide configuration

+
+ +
{ e.preventDefault(); put('/admin/settings'); }}> +
+ {/* App */} + +

Application

+ + setData('app.name', e.target.value)} /> + + + + +
+ + {/* Security */} + +

Security

+ + setData('security.session_timeout', e.target.value)} /> + + + + +
+ + {/* Mail */} + +

Email / Notifications

+ + + + + setData('mail.from', e.target.value)} placeholder="noreply@example.com" /> + +
+ +
+ +
+
+
+
+
+ ); +} diff --git a/resources/js/Pages/Auth/Login.jsx b/resources/js/Pages/Auth/Login.jsx new file mode 100644 index 0000000..c287e4a --- /dev/null +++ b/resources/js/Pages/Auth/Login.jsx @@ -0,0 +1,154 @@ +import { useForm, Head } from '@inertiajs/react'; +import { motion } from 'framer-motion'; +import { Zap, Lock, Mail, Eye, EyeOff, AlertCircle } from 'lucide-react'; +import { useState } from 'react'; +import { Button } from '@/Components/ui/Button'; +import { cn } from '@/lib/utils'; + +export default function Login({ ldapEnabled }) { + const { data, setData, post, processing, errors } = useForm({ + email: '', + password: '', + remember: false, + }); + const [showPass, setShowPass] = useState(false); + + const submit = (e) => { + e.preventDefault(); + post('/login'); + }; + + return ( + <> + +
+ {/* Background effects */} +
+
+
+ + {/* Meteors */} + {Array.from({ length: 8 }).map((_, i) => ( + + ))} + + + {/* Logo */} +
+
+ +
+

Infra Manager

+

Unified Infrastructure Control Plane

+
+ + {/* Card */} +
+ {/* Gradient border glow */} +
+ +
+
+ +
+ + setData('email', e.target.value)} + placeholder="admin@infra.local" + required + className={cn( + 'w-full pl-10 pr-4 py-2.5 bg-white/5 border rounded-xl text-sm text-white placeholder:text-white/20', + 'focus:outline-none focus:border-cyan-500/60 focus:bg-white/[0.07] transition-all', + errors.email ? 'border-red-500/60' : 'border-white/10' + )} + /> +
+ {errors.email && ( +

+ {errors.email} +

+ )} +
+ +
+ +
+ + setData('password', e.target.value)} + placeholder="••••••••" + required + className={cn( + 'w-full pl-10 pr-10 py-2.5 bg-white/5 border rounded-xl text-sm text-white placeholder:text-white/20', + 'focus:outline-none focus:border-cyan-500/60 focus:bg-white/[0.07] transition-all', + errors.password ? 'border-red-500/60' : 'border-white/10' + )} + /> + +
+
+ +
+ +
+ + + + {ldapEnabled && ( +
+
+ +
+
+ or +
+
+ )} +
+
+ +

+ Default: admin@infra.local / Admin@123456 +

+ +
+ + ); +} diff --git a/resources/js/Pages/Backup/Index.jsx b/resources/js/Pages/Backup/Index.jsx new file mode 100644 index 0000000..251745a --- /dev/null +++ b/resources/js/Pages/Backup/Index.jsx @@ -0,0 +1,114 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { HardDrive, Plus, Download, RotateCcw, Calendar, CheckCircle, XCircle, Clock } from 'lucide-react'; +import { GlassCard, StatCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { Button } from '@/Components/ui/Button'; +import { DataTable } from '@/Components/ui/DataTable'; +import { formatBytes, timeAgo } from '@/lib/utils'; + +export default function BackupIndex({ backups, policies, stats }) { + const backupColumns = [ + { header: 'Target', accessor: 'target_name', render: row => ( +
+

{row.target_name}

+

{row.target_type}

+
+ )}, + { header: 'Size', accessor: 'size_bytes', render: row => ( + {row.size_bytes ? formatBytes(row.size_bytes) : '—'} + )}, + { header: 'Status', accessor: 'status', render: row => }, + { header: 'Created', accessor: 'created_at', render: row => ( + {timeAgo(row.created_at)} + )}, + { header: 'Expires', accessor: 'expires_at', render: row => ( + row.expires_at + ? {timeAgo(row.expires_at)} + : Never + )}, + { header: '', key: 'actions', render: row => ( +
e.stopPropagation()}> + {row.status === 'success' && ( + <> +
+ )}, + ]; + + const policyColumns = [ + { header: 'Policy', accessor: 'name', render: row => ( +
+

{row.name}

+

{row.target_type} · {row.schedule}

+
+ )}, + { header: 'Retention', accessor: 'retention_value', render: row => ( + {row.retention_value} {row.retention_type === 'count' ? 'backups' : 'days'} + )}, + { header: 'Storage', accessor: 'storage_type', render: row => ( + {row.storage_type} + )}, + { header: 'Status', accessor: 'is_active', render: row => ( + + )}, + ]; + + return ( + + +
+
+
+

Backup & Disaster Recovery

+

Manage backups, schedules, and restore points

+
+
+ + +
+
+ +
+ + + + +
+ + {/* DR Status */} +
+ {[ + { label: 'RTO', value: '< 4h', desc: 'Recovery Time Objective', color: 'text-green-400' }, + { label: 'RPO', value: '< 1h', desc: 'Recovery Point Objective', color: 'text-cyan-400' }, + { label: 'Last DR Test', value: 'N/A', desc: 'Last tested', color: 'text-white/40' }, + ].map(d => ( + +

{d.label}

+

{d.value}

+

{d.desc}

+
+ ))} +
+ + +

Backup Policies

+ +
+ + +

Backup History

+ +
+
+
+ ); +} diff --git a/resources/js/Pages/Cloud/Index.jsx b/resources/js/Pages/Cloud/Index.jsx new file mode 100644 index 0000000..9ea010d --- /dev/null +++ b/resources/js/Pages/Cloud/Index.jsx @@ -0,0 +1,150 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { Cloud, Plus, RefreshCw, Server, HardDrive, Database, Globe } from 'lucide-react'; +import { GlassCard, StatCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { Button } from '@/Components/ui/Button'; +import { DataTable } from '@/Components/ui/DataTable'; +import { timeAgo } from '@/lib/utils'; +import { motion } from 'framer-motion'; +import { useState } from 'react'; + +const PROVIDERS = { + aws: { label: 'Amazon Web Services', color: '#FF9900', icon: '☁' }, + alibaba: { label: 'Alibaba Cloud', color: '#FF6A00', icon: '☁' }, + tencent: { label: 'Tencent Cloud', color: '#00A3FF', icon: '☁' }, + gcp: { label: 'Google Cloud', color: '#4285F4', icon: '☁' }, + azure: { label: 'Microsoft Azure', color: '#0078D4', icon: '☁' }, + digitalocean: { label: 'DigitalOcean', color: '#0080FF', icon: '☁' }, + vultr: { label: 'Vultr', color: '#007BFC', icon: '☁' }, + hetzner: { label: 'Hetzner', color: '#D50C2D', icon: '☁' }, +}; + +const RESOURCE_ICONS = { ec2: Server, s3: HardDrive, rds: Database, vpc: Globe }; + +export default function CloudIndex({ accounts, resources, stats }) { + const [syncLoading, setSyncLoading] = useState(null); + const [selectedAccount, setSelectedAccount] = useState(null); + + const sync = (id) => { + setSyncLoading(id); + router.post(`/cloud/accounts/${id}/sync`, {}, { onFinish: () => setSyncLoading(null) }); + }; + + const accountColumns = [ + { header: 'Account', accessor: 'name', render: row => { + const p = PROVIDERS[row.provider] ?? {}; + return ( +
+
{row.provider?.toUpperCase().slice(0, 2)}
+
+

{row.name}

+

{p.label ?? row.provider}

+
+
+ ); + }}, + { header: 'Account ID', accessor: 'account_id', render: row => ( + {row.account_id ?? '—'} + )}, + { header: 'Region', accessor: 'region', render: row => ( + {row.region ?? '—'} + )}, + { header: 'Status', accessor: 'status', render: row => }, + { header: 'Last Sync', accessor: 'last_sync_at', render: row => ( + {timeAgo(row.last_sync_at)} + )}, + { header: '', key: 'actions', render: row => ( +
e.stopPropagation()}> + +
+ )}, + ]; + + const resourceColumns = [ + { header: 'Resource', accessor: 'name', render: row => { + const Icon = RESOURCE_ICONS[row.resource_type] ?? Cloud; + return ( +
+ +
+

{row.name}

+

{row.resource_id}

+
+
+ ); + }}, + { header: 'Type', accessor: 'resource_type', render: row => ( + {row.resource_type} + )}, + { header: 'Region', accessor: 'region', render: row => ( + {row.region} + )}, + { header: 'Status', accessor: 'status', render: row => }, + { header: 'Cost/mo', accessor: 'monthly_cost', render: row => ( + row.monthly_cost ? ${row.monthly_cost} : — + )}, + ]; + + return ( + + +
+
+
+

Cloud Management

+

AWS, Alibaba, Tencent, GCP, Azure and more

+
+ + + +
+ + {/* Provider cards */} +
+ {Object.entries(PROVIDERS).slice(0, 4).map(([key, p]) => { + const count = accounts?.filter(a => a.provider === key).length ?? 0; + return ( + +
+
+ {key.toUpperCase().slice(0, 2)} +
+
+

{key.toUpperCase()}

+

{count} account{count !== 1 ? 's' : ''}

+
+
+
+ ); + })} +
+ + {/* Accounts */} + +

Cloud Accounts

+ router.visit(`/cloud/accounts/${row.id}`)} + emptyMessage="No cloud accounts. Add your first account to get started." + /> +
+ + {/* Resources */} + +

Cloud Resources

+ +
+
+
+ ); +} diff --git a/resources/js/Pages/Container/Index.jsx b/resources/js/Pages/Container/Index.jsx new file mode 100644 index 0000000..6a545ae --- /dev/null +++ b/resources/js/Pages/Container/Index.jsx @@ -0,0 +1,131 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { motion } from 'framer-motion'; +import { Box, Play, Square, RotateCcw, Trash2, Terminal, BarChart2, Plus } from 'lucide-react'; +import { GlassCard, StatCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { Button } from '@/Components/ui/Button'; +import { DataTable } from '@/Components/ui/DataTable'; +import { useState } from 'react'; +import { cn } from '@/lib/utils'; + +export default function ContainerIndex({ containers, endpoints }) { + const [actionLoading, setActionLoading] = useState(null); + const [selectedEndpoint, setSelectedEndpoint] = useState(null); + + const action = (containerId, act) => { + setActionLoading(`${containerId}-${act}`); + router.post(`/containers/${containerId}/action`, { action: act }, { + onFinish: () => setActionLoading(null), + }); + }; + + const filtered = selectedEndpoint + ? (containers.data ?? []).filter(c => c.endpoint_id === selectedEndpoint) + : (containers.data ?? []); + + const columns = [ + { header: 'Container', accessor: 'name', render: row => ( +
+
+ +
+
+

{row.name}

+

{row.image}

+
+
+ )}, + { header: 'Status', accessor: 'state', render: row => }, + { header: 'Ports', key: 'ports', render: row => ( +
+ {(row.ports ?? []).slice(0, 3).map((p, i) => ( + + {p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}` : p.PrivatePort} + + ))} + {(row.ports ?? []).length > 3 && +{row.ports.length - 3}} +
+ )}, + { header: '', key: 'actions', render: row => ( +
e.stopPropagation()}> + {row.state === 'running' ? ( + <> + + + + ) : ( + + )} + + + + + + +
+ )}, + ]; + + const running = (containers.data ?? []).filter(c => c.state === 'running').length; + const stopped = (containers.data ?? []).filter(c => c.state !== 'running').length; + + return ( + + +
+
+
+

Containers

+

Manage Docker containers across all endpoints

+
+ +
+ +
+ + + + +
+ + {/* Endpoint filter tabs */} + {endpoints?.length > 0 && ( +
+ + {endpoints.map(e => ( + + ))} +
+ )} + + + router.visit(`/containers/${row.id}`)} + emptyMessage="No containers found. Add a Docker endpoint to get started." + /> + +
+
+ ); +} diff --git a/resources/js/Pages/Dashboard/Index.jsx b/resources/js/Pages/Dashboard/Index.jsx new file mode 100644 index 0000000..a45a008 --- /dev/null +++ b/resources/js/Pages/Dashboard/Index.jsx @@ -0,0 +1,238 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head } from '@inertiajs/react'; +import { motion } from 'framer-motion'; +import { + Server, Monitor, Box, Cloud, Shield, AlertTriangle, + Activity, TrendingUp, Cpu, MemoryStick, HardDrive, Wifi +} from 'lucide-react'; +import { StatCard, GlassCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { ProgressBar } from '@/Components/ui/ProgressBar'; +import { timeAgo, getStatusDot } from '@/lib/utils'; +import { + AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, + ResponsiveContainer, PieChart, Pie, Cell +} from 'recharts'; + +const FADE_UP = { hidden: { opacity: 0, y: 20 }, show: { opacity: 1, y: 0 } }; +const CONTAINER = { + hidden: {}, + show: { transition: { staggerChildren: 0.05 } } +}; + +// Mock chart data - replace with real metrics +const cpuData = Array.from({ length: 20 }, (_, i) => ({ + t: `${i}m`, cpu: Math.round(20 + Math.random() * 50), mem: Math.round(40 + Math.random() * 40) +})); + +const PROVIDER_COLORS = { aws: '#FF9900', alibaba: '#FF6A00', tencent: '#00A3FF', gcp: '#4285F4', azure: '#0078D4' }; + +export default function Dashboard({ stats, hypervisors, endpoints, cloud_accounts, recent_alerts }) { + return ( + + + + + + {/* ── Header ──────────────────────────────────────── */} + +
+

Infrastructure Overview

+

Real-time view across all platforms

+
+
+ + Live +
+
+ + {/* ── Top stats ────────────────────────────────────── */} + + + + + + 0 ? 'red' : 'green'} /> + = 80 ? 'green' : stats.security_score >= 60 ? 'yellow' : 'red'} /> + + + {/* ── Charts Row ───────────────────────────────────── */} + + {/* CPU / Memory chart */} + +
+
+

Resource Utilization

+

Avg CPU & Memory — last 20 min

+
+
+ CPU + Memory +
+
+ + + + + + + + + + + + + + + + + + + + +
+ + {/* VM Status Donut */} + +

VM Status

+

Across all hypervisors

+ + + + + + + + + + +
+ {[{ label: 'Running', val: stats.vms_running, color: 'bg-cyan-400' }, + { label: 'Stopped', val: stats.vms_stopped, color: 'bg-slate-600' }].map(s => ( +
+
{s.label}
+ {s.val} +
+ ))} +
+
+
+ + {/* ── Hypervisors & Endpoints ───────────────────────── */} + + {/* Hypervisors */} + +
+

Hypervisors

+ View all → +
+
+ {hypervisors?.slice(0, 6).map(h => ( + + +
+

{h.name}

+

{h.type} · {timeAgo(h.last_sync_at)}

+
+ +
+ ))} + {(!hypervisors || hypervisors.length === 0) && ( +

No hypervisors configured

+ )} +
+
+ + {/* Docker Endpoints */} + +
+

Docker Endpoints

+ View all → +
+
+ {endpoints?.slice(0, 6).map(e => ( +
+ +
+

{e.name}

+

{e.type}

+
+ +
+ ))} + {(!endpoints || endpoints.length === 0) && ( +

No Docker endpoints

+ )} +
+
+
+ + {/* ── Cloud & Alerts ────────────────────────────────── */} + + {/* Cloud accounts */} + +
+

Cloud Accounts

+ Manage → +
+
+ {cloud_accounts?.map(acc => ( +
+
+ {acc.provider?.toUpperCase().slice(0, 2)} +
+
+

{acc.name}

+ +
+
+ ))} + {(!cloud_accounts || cloud_accounts.length === 0) && ( +

No cloud accounts

+ )} +
+
+ + {/* Recent Alerts */} + +
+

Active Alerts

+ View all → +
+
+ {recent_alerts?.map(alert => ( +
+ +
+

{alert.title}

+

{alert.target_name} · {timeAgo(alert.created_at)}

+
+ +
+ ))} + {(!recent_alerts || recent_alerts.length === 0) && ( +
+ +

All clear — no active alerts

+
+ )} +
+
+
+
+
+ ); +} diff --git a/resources/js/Pages/Error.jsx b/resources/js/Pages/Error.jsx new file mode 100644 index 0000000..acc50fa --- /dev/null +++ b/resources/js/Pages/Error.jsx @@ -0,0 +1,39 @@ +import { Head } from '@inertiajs/react'; +import { motion } from 'framer-motion'; +import { Zap, AlertTriangle } from 'lucide-react'; + +const MESSAGES = { + 403: { title: 'Access Denied', desc: 'You do not have permission to access this resource.' }, + 404: { title: 'Page Not Found', desc: 'The page you are looking for does not exist.' }, + 500: { title: 'Server Error', desc: 'Something went wrong on our end. Please try again.' }, + 503: { title: 'Unavailable', desc: 'The service is temporarily unavailable.' }, +}; + +export default function Error({ status }) { + const { title, desc } = MESSAGES[status] ?? { title: 'Error', desc: 'An unexpected error occurred.' }; + + return ( + <> + +
+
+ +
+ +
+

{status}

+

{title}

+

{desc}

+ + + Back to Dashboard + +
+
+ + ); +} diff --git a/resources/js/Pages/Hypervisor/Create.jsx b/resources/js/Pages/Hypervisor/Create.jsx new file mode 100644 index 0000000..87bbcd0 --- /dev/null +++ b/resources/js/Pages/Hypervisor/Create.jsx @@ -0,0 +1,163 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, useForm, router } from '@inertiajs/react'; +import { useState } from 'react'; +import { motion } from 'framer-motion'; +import { Server, CheckCircle, XCircle, Loader2 } from 'lucide-react'; +import { GlassCard } from '@/Components/ui/GlassCard'; +import { Button } from '@/Components/ui/Button'; +import { cn } from '@/lib/utils'; + +const TYPES = [ + { value: 'vcenter', label: 'VMware vCenter', port: 443 }, + { value: 'esxi', label: 'VMware ESXi', port: 443 }, + { value: 'proxmox', label: 'Proxmox VE', port: 8006 }, + { value: 'hyper-v', label: 'Hyper-V', port: 5985 }, + { value: 'xen', label: 'XenServer', port: 443 }, + { value: 'kvm', label: 'KVM/QEMU', port: 22 }, +]; + +function Field({ label, error, children }) { + return ( +
+ + {children} + {error &&

{error}

} +
+ ); +} + +function Input({ className, ...props }) { + return ( + + ); +} + +export default function HypervisorCreate() { + const { data, setData, post, processing, errors } = useForm({ + name: '', type: 'vcenter', host: '', port: 443, + username: '', password: '', ssl_verify: true, datacenter: '', + }); + const [testResult, setTestResult] = useState(null); + const [testing, setTesting] = useState(false); + + const handleTypeChange = (type) => { + const t = TYPES.find(t => t.value === type); + setData(d => ({ ...d, type, port: t?.port ?? 443 })); + }; + + const testConn = async () => { + setTesting(true); + setTestResult(null); + try { + const res = await fetch('/hypervisors/test-temp', { + method: 'POST', + headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': document.querySelector('meta[name=csrf-token]').content }, + body: JSON.stringify(data), + }); + const json = await res.json(); + setTestResult(json); + } catch (e) { + setTestResult({ success: false, message: 'Connection failed: ' + e.message }); + } finally { + setTesting(false); + } + }; + + return ( + + +
+
+

Add Hypervisor

+

Connect a new virtualization platform

+
+ + +
{ e.preventDefault(); post('/hypervisors'); }} className="space-y-5"> + {/* Type selector */} +
+ +
+ {TYPES.map(t => ( + + ))} +
+
+ + + setData('name', e.target.value)} placeholder="Production vCenter" required /> + + +
+
+ + setData('host', e.target.value)} placeholder="vcenter.example.com" required /> + +
+ + setData('port', parseInt(e.target.value))} /> + +
+ +
+ + setData('username', e.target.value)} placeholder="administrator@vsphere.local" required /> + + + setData('password', e.target.value)} required /> + +
+ + + setData('datacenter', e.target.value)} placeholder="Datacenter name" /> + + + + + {/* Test Result */} + {testResult && ( + + {testResult.success + ? + : } + {testResult.message} + + )} + +
+ +
+ + +
+
+
+
+
+
+ ); +} diff --git a/resources/js/Pages/Hypervisor/Index.jsx b/resources/js/Pages/Hypervisor/Index.jsx new file mode 100644 index 0000000..897a240 --- /dev/null +++ b/resources/js/Pages/Hypervisor/Index.jsx @@ -0,0 +1,114 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { motion } from 'framer-motion'; +import { Plus, RefreshCw, Server, TestTube, Edit, Trash2, Eye } from 'lucide-react'; +import { GlassCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { Button } from '@/Components/ui/Button'; +import { DataTable } from '@/Components/ui/DataTable'; +import { timeAgo } from '@/lib/utils'; +import { useState } from 'react'; + +const TYPE_LABELS = { + vcenter: 'VMware vCenter', esxi: 'VMware ESXi', proxmox: 'Proxmox VE', + 'hyper-v': 'Hyper-V', xen: 'XenServer', kvm: 'KVM', +}; +const TYPE_COLORS = { + vcenter: 'text-blue-400', esxi: 'text-blue-300', proxmox: 'text-orange-400', + 'hyper-v': 'text-blue-500', kvm: 'text-green-400', +}; + +export default function HypervisorIndex({ hypervisors }) { + const [syncing, setSyncing] = useState(null); + + const sync = (id) => { + setSyncing(id); + router.post(`/hypervisors/${id}/sync`, {}, { + onFinish: () => setSyncing(null), + }); + }; + + const columns = [ + { header: 'Name', accessor: 'name', render: row => ( +
+
+ +
+
+

{row.name}

+

{TYPE_LABELS[row.type] ?? row.type}

+
+
+ )}, + { header: 'Host', accessor: 'host', render: row => ( + {row.host}:{row.port} + )}, + { header: 'VMs', key: 'vms', render: row => ( + {row.virtual_machines_count ?? 0} + )}, + { header: 'Status', accessor: 'status', render: row => }, + { header: 'Last Sync', accessor: 'last_sync_at', render: row => ( + {timeAgo(row.last_sync_at)} + )}, + { header: '', key: 'actions', render: row => ( +
e.stopPropagation()}> + + + + + + + + +
+ )}, + ]; + + return ( + + +
+
+
+

Hypervisors

+

Manage vCenter, Proxmox, ESXi, Hyper-V and more

+
+ + + +
+ + {/* Summary Cards */} +
+ {[ + { label: 'Total', value: hypervisors.total ?? 0, color: 'text-white' }, + { label: 'Online', value: hypervisors.data?.filter(h => h.status === 'online').length ?? 0, color: 'text-green-400' }, + { label: 'Offline', value: hypervisors.data?.filter(h => h.status === 'offline').length ?? 0, color: 'text-red-400' }, + { label: 'Error', value: hypervisors.data?.filter(h => h.status === 'error').length ?? 0, color: 'text-orange-400' }, + ].map(s => ( + +

{s.value}

+

{s.label}

+
+ ))} +
+ + + router.visit(`/hypervisors/${row.id}`)} + emptyMessage="No hypervisors configured. Add your first one!" + /> + +
+
+ ); +} diff --git a/resources/js/Pages/Identity/Index.jsx b/resources/js/Pages/Identity/Index.jsx new file mode 100644 index 0000000..958dd9a --- /dev/null +++ b/resources/js/Pages/Identity/Index.jsx @@ -0,0 +1,161 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head } from '@inertiajs/react'; +import { KeyRound, Lock, Certificate, Key, Plus, Eye, EyeOff, Copy, AlertCircle } from 'lucide-react'; +import { GlassCard, StatCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { Button } from '@/Components/ui/Button'; +import { DataTable } from '@/Components/ui/DataTable'; +import { timeAgo } from '@/lib/utils'; +import { useState } from 'react'; +import { Tabs, TabsList, TabsTrigger, TabsContent } from '@radix-ui/react-tabs'; +import { cn } from '@/lib/utils'; + +export default function IdentityIndex({ passwords, certificates, ssh_keys, stats }) { + const [activeTab, setActiveTab] = useState('passwords'); + + const passwordColumns = [ + { header: 'Title', accessor: 'title', render: row => ( +
+

{row.title}

+ {row.url &&

{row.url}

} +
+ )}, + { header: 'Username', accessor: 'username', render: row => ( + {row.username ?? '—'} + )}, + { header: 'Category', accessor: 'category', render: row => ( + row.category ? {row.category} : null + )}, + { header: 'Expires', accessor: 'expires_at', render: row => ( + row.expires_at + ? {timeAgo(row.expires_at)} + : Never + )}, + { header: '', key: 'actions', render: row => ( +
+ + +
+ )}, + ]; + + const certColumns = [ + { header: 'Domain', accessor: 'domain', render: row => ( +
+

{row.domain}

+

{row.issuer ?? 'Self-signed'}

+
+ )}, + { header: 'Type', accessor: 'type', render: row => ( + {row.type?.replace('-', ' ')} + )}, + { header: 'Expires', accessor: 'expires_at', render: row => { + const daysLeft = Math.ceil((new Date(row.expires_at) - new Date()) / 86400000); + return ( +
+

+ {daysLeft > 0 ? `${daysLeft}d left` : 'Expired'} +

+

{new Date(row.expires_at).toLocaleDateString()}

+
+ ); + }}, + { header: 'Status', accessor: 'status', render: row => }, + { header: '', key: 'actions', render: row => ( + + )}, + ]; + + return ( + + +
+
+
+

Identity & Secrets

+

Passwords, certificates, SSH keys, and secrets

+
+
+ +
+ + + + +
+ + {/* Tabs */} + + + {[ + { value: 'passwords', label: 'Passwords', icon: Lock }, + { value: 'certificates', label: 'Certificates', icon: KeyRound }, + { value: 'ssh-keys', label: 'SSH Keys', icon: Key }, + ].map(t => { + const Icon = t.icon; + return ( + + + {t.label} + + ); + })} + + + + +
+

Password Vault

+ +
+ +
+
+ + + +
+

SSL/TLS Certificates

+
+ + +
+
+ +
+
+ + + +
+

SSH Keys

+
+ + +
+
+

{r.name}

}, + { header: 'Algorithm', accessor: 'algorithm', render: r => {r.algorithm?.toUpperCase()} {r.key_size} }, + { header: 'Fingerprint', accessor: 'fingerprint', render: r => {r.fingerprint} }, + { header: '', key: 'a', render: r => }, + ]} + data={ssh_keys ?? []} + emptyMessage="No SSH keys. Generate or import a key pair." + /> +
+
+
+
+
+ ); +} diff --git a/resources/js/Pages/Monitoring/Index.jsx b/resources/js/Pages/Monitoring/Index.jsx new file mode 100644 index 0000000..1582527 --- /dev/null +++ b/resources/js/Pages/Monitoring/Index.jsx @@ -0,0 +1,131 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head } from '@inertiajs/react'; +import { motion } from 'framer-motion'; +import { Activity, Bell, CheckCircle, AlertTriangle, XCircle, Plus } from 'lucide-react'; +import { GlassCard, StatCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { Button } from '@/Components/ui/Button'; +import { DataTable } from '@/Components/ui/DataTable'; +import { ProgressBar } from '@/Components/ui/ProgressBar'; +import { timeAgo } from '@/lib/utils'; +import { + LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, + ResponsiveContainer, AreaChart, Area +} from 'recharts'; + +// Demo metric data +const metricStream = Array.from({ length: 30 }, (_, i) => ({ + t: `-${30 - i}m`, + cpu: 30 + Math.sin(i * 0.3) * 20 + Math.random() * 10, + mem: 55 + Math.cos(i * 0.2) * 15 + Math.random() * 8, + net: Math.abs(Math.sin(i * 0.5) * 400 + Math.random() * 100), +})); + +export default function MonitoringIndex({ alerts, alert_stats, dashboards }) { + const firing = alerts?.filter(a => a.status === 'firing') ?? []; + const acked = alerts?.filter(a => a.status === 'acknowledged') ?? []; + + const alertColumns = [ + { header: 'Alert', accessor: 'title', render: row => ( +
+

{row.title}

+

{row.target_name}

+
+ )}, + { header: 'Severity', accessor: 'severity', render: row => ( + + )}, + { header: 'Status', accessor: 'status', render: row => ( + + )}, + { header: 'Fired', accessor: 'created_at', render: row => ( + {timeAgo(row.created_at)} + )}, + { header: '', key: 'actions', render: row => ( + row.status === 'firing' && ( + + ) + )}, + ]; + + return ( + + +
+
+
+

Unified Monitoring

+

Real-time metrics, alerts, and dashboards

+
+ +
+ + {/* Alert Stats */} +
+ + + + +
+ + {/* Charts */} +
+ +

CPU & Memory

+

Average across all VMs — last 30 min

+ + + + + + + + + + + + + + + + + + + + +
+ + +

Network I/O

+

Mbps across all endpoints

+ + + + + + + + + +
+
+ + {/* Active Alerts Table */} + +
+

Active Alerts

+ +
+ +
+
+
+ ); +} diff --git a/resources/js/Pages/Security/Index.jsx b/resources/js/Pages/Security/Index.jsx new file mode 100644 index 0000000..d6c94b8 --- /dev/null +++ b/resources/js/Pages/Security/Index.jsx @@ -0,0 +1,138 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { motion } from 'framer-motion'; +import { Shield, AlertTriangle, CheckCircle, TrendingUp, Scan, Lock, Bug } from 'lucide-react'; +import { GlassCard, StatCard } from '@/Components/ui/GlassCard'; +import { StatusBadge } from '@/Components/ui/StatusBadge'; +import { Button } from '@/Components/ui/Button'; +import { ProgressBar } from '@/Components/ui/ProgressBar'; +import { DataTable } from '@/Components/ui/DataTable'; +import { timeAgo, getSecurityGradeColor } from '@/lib/utils'; +import { RadarChart, Radar, PolarGrid, PolarAngleAxis, ResponsiveContainer } from 'recharts'; +import { cn } from '@/lib/utils'; + +const radarData = [ + { subject: 'Patching', A: 85 }, + { subject: 'Access Ctrl', A: 72 }, + { subject: 'Encryption', A: 90 }, + { subject: 'Network', A: 65 }, + { subject: 'Monitoring', A: 78 }, + { subject: 'Compliance', A: 60 }, +]; + +function ScoreRing({ score, grade }) { + const color = grade === 'A' ? '#00ff88' : grade === 'B' ? '#00d4ff' : grade === 'C' ? '#ffd700' : grade === 'D' ? '#ff6b35' : '#ff2d55'; + const r = 60, circumference = 2 * Math.PI * r; + const dash = (score / 100) * circumference; + return ( +
+ + + + +
+

{score}

+

/ 100

+
+
+ ); +} + +export default function SecurityIndex({ scores, scans, stats }) { + const avgScore = scores?.avg ?? 72; + const grade = avgScore >= 90 ? 'A' : avgScore >= 75 ? 'B' : avgScore >= 60 ? 'C' : avgScore >= 45 ? 'D' : 'F'; + + const scanColumns = [ + { header: 'Target', accessor: 'target_name', render: row => ( +
+

{row.target_name}

+

{row.target_type} · {row.scan_type}

+
+ )}, + { header: 'Score', accessor: 'score', render: row => ( +
+ {row.grade} + {row.score ?? '—'} +
+ )}, + { header: 'Findings', key: 'findings', render: row => ( +
+ {row.critical_count > 0 && {row.critical_count}C} + {row.high_count > 0 && {row.high_count}H} + {row.medium_count > 0 && {row.medium_count}M} + {row.low_count > 0 && {row.low_count}L} +
+ )}, + { header: 'Status', accessor: 'status', render: row => }, + { header: 'Scanned', accessor: 'completed_at', render: row => ( + {timeAgo(row.completed_at)} + )}, + ]; + + return ( + + +
+
+
+

Cybersecurity Dashboard

+

Security posture across all infrastructure

+
+ +
+ + {/* Score + Radar */} +
+ +

Overall Security Score

+ +
+ Grade {grade} +
+

Based on {scans?.total ?? 0} scans across infrastructure

+
+ + +

Security Radar

+

Coverage across security domains

+ + + + + + + +
+
+ + {/* Stats */} +
+ + + + +
+ + {/* Scan results table */} + +
+

Recent Scans

+ +
+ +
+
+
+ ); +} diff --git a/resources/js/Pages/Wiki/Index.jsx b/resources/js/Pages/Wiki/Index.jsx new file mode 100644 index 0000000..805dea5 --- /dev/null +++ b/resources/js/Pages/Wiki/Index.jsx @@ -0,0 +1,113 @@ +import AppLayout from '@/Layouts/AppLayout'; +import { Head, router } from '@inertiajs/react'; +import { BookOpen, Plus, FileText, Folder, Upload, Search } from 'lucide-react'; +import { GlassCard } from '@/Components/ui/GlassCard'; +import { Button } from '@/Components/ui/Button'; +import { motion } from 'framer-motion'; +import { timeAgo } from '@/lib/utils'; +import { useState } from 'react'; + +export default function WikiIndex({ spaces, recent_pages, files }) { + const [search, setSearch] = useState(''); + + const filtered = (recent_pages ?? []).filter(p => + p.title.toLowerCase().includes(search.toLowerCase()) + ); + + return ( + + +
+
+
+

Wiki & Documents

+

Centralized documentation, runbooks, and files

+
+
+ + +
+
+ + {/* Search */} +
+ + setSearch(e.target.value)} + placeholder="Search pages, documents..." + className="w-full pl-12 pr-4 py-3 bg-white/5 border border-white/10 rounded-xl text-sm text-white placeholder:text-white/20 focus:outline-none focus:border-cyan-500/50 transition-all" + /> +
+ +
+ {/* Spaces */} +
+
+

Spaces

+ +
+
+ {(spaces ?? []).map(space => ( + + +
+
+ +
+
+

{space.name}

+

{space.pages_count ?? 0} pages

+
+
+
+
+ ))} + {(!spaces || spaces.length === 0) && ( + + +

No spaces yet

+
+ )} +
+
+ + {/* Recent Pages */} +
+

+ {search ? `Search: "${search}"` : 'Recent Pages'} +

+
+ {filtered.map(page => ( + + +
+ +
+

{page.title}

+
+

{page.space?.name}

+ · +

Updated {timeAgo(page.updated_at)}

+
+
+
+
+
+ ))} + {filtered.length === 0 && ( + + +

+ {search ? 'No pages match your search' : 'No pages yet. Create your first page!'} +

+
+ )} +
+
+
+
+
+ ); +} diff --git a/resources/js/app.jsx b/resources/js/app.jsx new file mode 100644 index 0000000..7585311 --- /dev/null +++ b/resources/js/app.jsx @@ -0,0 +1,24 @@ +import './bootstrap'; +import '../css/app.css'; + +import { createRoot } from 'react-dom/client'; +import { createInertiaApp } from '@inertiajs/react'; +import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; + +createInertiaApp({ + title: (title) => `${title} — Infra Manager`, + resolve: (name) => + resolvePageComponent( + `./Pages/${name}.jsx`, + import.meta.glob('./Pages/**/*.jsx') + ), + setup({ el, App, props }) { + const root = createRoot(el); + root.render(); + }, + progress: { + color: '#00d4ff', + includeCSS: true, + showSpinner: true, + }, +}); diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js new file mode 100644 index 0000000..7d62edc --- /dev/null +++ b/resources/js/bootstrap.js @@ -0,0 +1,4 @@ +import axios from 'axios'; + +window.axios = axios; +window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; diff --git a/resources/js/lib/utils.js b/resources/js/lib/utils.js new file mode 100644 index 0000000..1ab0933 --- /dev/null +++ b/resources/js/lib/utils.js @@ -0,0 +1,71 @@ +import { clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +export function cn(...inputs) { + return twMerge(clsx(inputs)); +} + +export function formatBytes(bytes, decimals = 2) { + if (!bytes || bytes === 0) return '0 Bytes'; + const k = 1024; + const dm = decimals < 0 ? 0 : decimals; + const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; +} + +export function formatUptime(seconds) { + const d = Math.floor(seconds / 86400); + const h = Math.floor((seconds % 86400) / 3600); + const m = Math.floor((seconds % 3600) / 60); + if (d > 0) return `${d}d ${h}h`; + if (h > 0) return `${h}h ${m}m`; + return `${m}m`; +} + +export function getStatusColor(status) { + const map = { + online: 'text-green-400', + running: 'text-cyan-400', + active: 'text-green-400', + stopped: 'text-slate-400', + offline: 'text-red-400', + error: 'text-red-400', + warning: 'text-yellow-400', + unknown: 'text-gray-400', + paused: 'text-yellow-400', + exited: 'text-slate-400', + }; + return map[status?.toLowerCase()] ?? 'text-gray-400'; +} + +export function getStatusDot(status) { + const map = { + online: 'bg-green-400', + running: 'bg-cyan-400', + active: 'bg-green-400', + stopped: 'bg-slate-500', + offline: 'bg-red-400', + error: 'bg-red-400', + warning: 'bg-yellow-400', + unknown: 'bg-gray-500', + paused: 'bg-yellow-400', + exited: 'bg-slate-500', + }; + return map[status?.toLowerCase()] ?? 'bg-gray-500'; +} + +export function getSecurityGradeColor(grade) { + const map = { A: 'text-green-400', B: 'text-cyan-400', C: 'text-yellow-400', D: 'text-orange-400', F: 'text-red-400' }; + return map[grade] ?? 'text-gray-400'; +} + +export function timeAgo(date) { + if (!date) return 'Never'; + const d = new Date(date); + const seconds = Math.floor((new Date() - d) / 1000); + if (seconds < 60) return `${seconds}s ago`; + if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`; + if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`; + return `${Math.floor(seconds / 86400)}d ago`; +} diff --git a/resources/views/app.blade.php b/resources/views/app.blade.php new file mode 100644 index 0000000..c173b15 --- /dev/null +++ b/resources/views/app.blade.php @@ -0,0 +1,24 @@ + + + + + + + + {{ config('app.name', 'Infra Manager') }} + + + + + + + + @routes + @viteReactRefresh + @vite(['resources/js/app.jsx', 'resources/css/app.css']) + @inertiaHead + + + @inertia + + diff --git a/routes/api.php b/routes/api.php new file mode 100644 index 0000000..99205fd --- /dev/null +++ b/routes/api.php @@ -0,0 +1,10 @@ +group(function () { + Route::get('/metrics/{target}', [MetricsController::class, 'live']); + Route::get('/terminal/{vm}', [WebSocketController::class, 'vmConsole']); +}); diff --git a/routes/console.php b/routes/console.php new file mode 100644 index 0000000..1af7fcc --- /dev/null +++ b/routes/console.php @@ -0,0 +1,16 @@ +comment(Inspiring::quote()); +})->purpose('Display an inspiring quote'); + +Schedule::command('infra:sync-hypervisors')->everyFiveMinutes(); +Schedule::command('infra:sync-containers')->everyThreeMinutes(); +Schedule::command('infra:sync-cloud')->everyTenMinutes(); +Schedule::command('infra:collect-metrics')->everyMinute(); +Schedule::command('infra:check-certificates')->daily(); +Schedule::command('infra:check-backups')->hourly(); diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..a0b5106 --- /dev/null +++ b/routes/web.php @@ -0,0 +1,205 @@ +group(function () { + Route::get('/login', [AuthController::class, 'showLogin'])->name('login'); + Route::post('/login', [AuthController::class, 'login']); +}); + +Route::post('/logout', [AuthController::class, 'logout'])->name('logout')->middleware('auth'); + +// ─── Authenticated Routes ──────────────────────────────────────────── +Route::middleware(['auth'])->group(function () { + + // Dashboard + Route::get('/', [DashboardController::class, 'index'])->name('dashboard'); + + // ── Hypervisor / vCenter ────────────────────────────────────── + Route::prefix('hypervisors')->name('hypervisors.')->group(function () { + Route::get('/', [HypervisorController::class, 'index'])->name('index'); + Route::get('/create', [HypervisorController::class, 'create'])->name('create'); + Route::post('/', [HypervisorController::class, 'store'])->name('store'); + Route::get('/{hypervisor}', [HypervisorController::class, 'show'])->name('show'); + Route::get('/{hypervisor}/edit', [HypervisorController::class, 'edit'])->name('edit'); + Route::put('/{hypervisor}', [HypervisorController::class, 'update'])->name('update'); + Route::delete('/{hypervisor}', [HypervisorController::class, 'destroy'])->name('destroy'); + Route::post('/{hypervisor}/sync', [HypervisorController::class, 'sync'])->name('sync'); + Route::post('/{hypervisor}/test-connection', [HypervisorController::class, 'testConnection'])->name('test-connection'); + }); + + Route::prefix('virtual-machines')->name('vms.')->group(function () { + Route::get('/', [VirtualMachineController::class, 'index'])->name('index'); + Route::get('/{vm}', [VirtualMachineController::class, 'show'])->name('show'); + Route::post('/{vm}/power', [VirtualMachineController::class, 'power'])->name('power'); + Route::post('/{vm}/snapshot', [VirtualMachineController::class, 'snapshot'])->name('snapshot'); + Route::post('/{vm}/clone', [VirtualMachineController::class, 'clone'])->name('clone'); + Route::post('/{vm}/migrate', [VirtualMachineController::class, 'migrate'])->name('migrate'); + }); + + Route::prefix('templates')->name('templates.')->group(function () { + Route::get('/', [TemplateController::class, 'index'])->name('index'); + Route::get('/create', [TemplateController::class, 'create'])->name('create'); + Route::post('/', [TemplateController::class, 'store'])->name('store'); + Route::get('/{template}', [TemplateController::class, 'show'])->name('show'); + Route::get('/{template}/edit', [TemplateController::class, 'edit'])->name('edit'); + Route::put('/{template}', [TemplateController::class, 'update'])->name('update'); + Route::delete('/{template}', [TemplateController::class, 'destroy'])->name('destroy'); + Route::post('/{template}/deploy', [TemplateController::class, 'deploy'])->name('deploy'); + }); + + // ── Containers / Docker ─────────────────────────────────────── + Route::prefix('containers')->name('containers.')->group(function () { + Route::get('/', [ContainerController::class, 'index'])->name('index'); + Route::get('/{container}', [ContainerController::class, 'show'])->name('show'); + Route::post('/{container}/action', [ContainerController::class, 'action'])->name('action'); + Route::get('/{container}/logs', [ContainerController::class, 'logs'])->name('logs'); + Route::get('/{container}/stats', [ContainerController::class, 'stats'])->name('stats'); + Route::delete('/{container}', [ContainerController::class, 'destroy'])->name('destroy'); + }); + + Route::prefix('stacks')->name('stacks.')->group(function () { + Route::get('/', [StackController::class, 'index'])->name('index'); + Route::get('/create', [StackController::class, 'create'])->name('create'); + Route::post('/', [StackController::class, 'store'])->name('store'); + Route::get('/{stack}', [StackController::class, 'show'])->name('show'); + Route::get('/{stack}/edit', [StackController::class, 'edit'])->name('edit'); + Route::put('/{stack}', [StackController::class, 'update'])->name('update'); + Route::delete('/{stack}', [StackController::class, 'destroy'])->name('destroy'); + Route::post('/{stack}/deploy', [StackController::class, 'deploy'])->name('deploy'); + }); + + // ── Cloud Accounts ──────────────────────────────────────────── + Route::prefix('cloud')->name('cloud.')->group(function () { + Route::get('/', [CloudAccountController::class, 'index'])->name('index'); + Route::get('/accounts/create', [CloudAccountController::class, 'create'])->name('accounts.create'); + Route::post('/accounts', [CloudAccountController::class, 'store'])->name('accounts.store'); + Route::get('/accounts/{account}', [CloudAccountController::class, 'show'])->name('accounts.show'); + Route::get('/accounts/{account}/edit', [CloudAccountController::class, 'edit'])->name('accounts.edit'); + Route::put('/accounts/{account}', [CloudAccountController::class, 'update'])->name('accounts.update'); + Route::delete('/accounts/{account}', [CloudAccountController::class, 'destroy'])->name('accounts.destroy'); + + Route::get('/resources', [CloudResourceController::class, 'index'])->name('resources.index'); + Route::get('/resources/{resource}', [CloudResourceController::class, 'show'])->name('resources.show'); + Route::post('/accounts/{account}/sync', [CloudAccountController::class, 'sync'])->name('accounts.sync'); + }); + + // ── Security Dashboard ──────────────────────────────────────── + Route::prefix('security')->name('security.')->group(function () { + Route::get('/', [SecurityDashboardController::class, 'index'])->name('index'); + Route::get('/scans', [ScanController::class, 'index'])->name('scans.index'); + Route::post('/scans', [ScanController::class, 'store'])->name('scans.store'); + Route::get('/scans/{scan}', [ScanController::class, 'show'])->name('scans.show'); + Route::get('/vulnerabilities', [SecurityDashboardController::class, 'vulnerabilities'])->name('vulnerabilities'); + Route::get('/compliance', [SecurityDashboardController::class, 'compliance'])->name('compliance'); + }); + + // ── Backup & DR ─────────────────────────────────────────────── + Route::prefix('backups')->name('backups.')->group(function () { + Route::get('/', [BackupController::class, 'index'])->name('index'); + Route::post('/', [BackupController::class, 'store'])->name('store'); + Route::get('/{backup}', [BackupController::class, 'show'])->name('show'); + Route::delete('/{backup}', [BackupController::class, 'destroy'])->name('destroy'); + Route::post('/{backup}/restore', [RestoreController::class, 'restore'])->name('restore'); + Route::get('/schedules', [BackupController::class, 'schedules'])->name('schedules'); + Route::post('/schedules', [BackupController::class, 'storeSchedule'])->name('schedules.store'); + }); + + // ── Monitoring ──────────────────────────────────────────────── + Route::prefix('monitoring')->name('monitoring.')->group(function () { + Route::get('/', [MonitoringController::class, 'index'])->name('index'); + Route::get('/metrics/{target}', [MonitoringController::class, 'metrics'])->name('metrics'); + Route::get('/alerts', [MonitoringController::class, 'alerts'])->name('alerts'); + Route::post('/alerts/{alert}/acknowledge', [MonitoringController::class, 'acknowledge'])->name('alerts.acknowledge'); + Route::get('/dashboards', [MonitoringController::class, 'dashboards'])->name('dashboards'); + }); + + // ── Identity / Secrets ──────────────────────────────────────── + Route::prefix('identity')->name('identity.')->group(function () { + Route::get('/', fn() => Inertia::render('Identity/Index'))->name('index'); + + Route::prefix('passwords')->name('passwords.')->group(function () { + Route::get('/', [PasswordController::class, 'index'])->name('index'); + Route::post('/', [PasswordController::class, 'store'])->name('store'); + Route::get('/{password}/reveal', [PasswordController::class, 'reveal'])->name('reveal'); + Route::put('/{password}', [PasswordController::class, 'update'])->name('update'); + Route::delete('/{password}', [PasswordController::class, 'destroy'])->name('destroy'); + }); + + Route::prefix('certificates')->name('certificates.')->group(function () { + Route::get('/', [CertificateController::class, 'index'])->name('index'); + Route::post('/', [CertificateController::class, 'store'])->name('store'); + Route::get('/{cert}', [CertificateController::class, 'show'])->name('show'); + Route::delete('/{cert}', [CertificateController::class, 'destroy'])->name('destroy'); + Route::post('/generate', [CertificateController::class, 'generate'])->name('generate'); + }); + }); + + // ── Wiki / Documents ────────────────────────────────────────── + Route::prefix('wiki')->name('wiki.')->group(function () { + Route::get('/', [WikiController::class, 'index'])->name('index'); + Route::get('/spaces', [WikiController::class, 'spaces'])->name('spaces'); + Route::get('/spaces/{space}', [WikiController::class, 'space'])->name('space'); + Route::get('/pages/create', [DocumentController::class, 'create'])->name('pages.create'); + Route::post('/pages', [DocumentController::class, 'store'])->name('pages.store'); + Route::get('/pages/{page}', [DocumentController::class, 'show'])->name('pages.show'); + Route::get('/pages/{page}/edit', [DocumentController::class, 'edit'])->name('pages.edit'); + Route::put('/pages/{page}', [DocumentController::class, 'update'])->name('pages.update'); + Route::delete('/pages/{page}', [DocumentController::class, 'destroy'])->name('pages.destroy'); + Route::get('/files', [WikiController::class, 'files'])->name('files'); + Route::post('/files', [WikiController::class, 'uploadFile'])->name('files.upload'); + Route::delete('/files/{file}', [WikiController::class, 'deleteFile'])->name('files.delete'); + }); + + // ── ACL / Roles & Users ─────────────────────────────────────── + Route::middleware(['role:admin|super-admin'])->group(function () { + Route::prefix('acl')->name('acl.')->group(function () { + Route::get('/', fn() => Inertia::render('Acl/Index'))->name('index'); + + Route::resource('roles', RoleController::class); + Route::resource('users', UserController::class); + Route::post('/users/{user}/roles', [UserController::class, 'assignRole'])->name('users.roles'); + }); + + // ── Administration ──────────────────────────────────────── + Route::prefix('admin')->name('admin.')->group(function () { + Route::get('/', [AdminController::class, 'index'])->name('index'); + Route::get('/settings', [SettingsController::class, 'index'])->name('settings'); + Route::put('/settings', [SettingsController::class, 'update'])->name('settings.update'); + Route::get('/audit-log', [AdminController::class, 'auditLog'])->name('audit-log'); + Route::get('/system-health', [AdminController::class, 'systemHealth'])->name('system-health'); + + Route::prefix('ldap')->name('ldap.')->group(function () { + Route::get('/', [LdapController::class, 'index'])->name('index'); + Route::put('/', [LdapController::class, 'update'])->name('update'); + Route::post('/test', [LdapController::class, 'test'])->name('test'); + Route::post('/sync', [LdapController::class, 'sync'])->name('sync'); + }); + }); + }); +}); diff --git a/setup.ps1 b/setup.ps1 new file mode 100644 index 0000000..bec6501 --- /dev/null +++ b/setup.ps1 @@ -0,0 +1,73 @@ +# Infra Manager — Windows PowerShell Setup Script +$ErrorActionPreference = "Stop" + +Write-Host "" +Write-Host "╔════════════════════════════════════════╗" -ForegroundColor Cyan +Write-Host "║ Infra Manager — Setup Script ║" -ForegroundColor Cyan +Write-Host "╚════════════════════════════════════════╝" -ForegroundColor Cyan +Write-Host "" + +# Check Docker +try { docker --version | Out-Null } catch { Write-Error "Docker not found. Install Docker Desktop first."; exit 1 } +Write-Host "[OK] Docker found" -ForegroundColor Green + +# Copy .env +if (-not (Test-Path ".env")) { + Copy-Item ".env.example" ".env" + Write-Host "[INFO] Created .env from .env.example" -ForegroundColor Cyan +} else { + Write-Host "[WARN] .env already exists" -ForegroundColor Yellow +} + +# Create SQLite DB +New-Item -ItemType Directory -Force -Path "database" | Out-Null +if (-not (Test-Path "database\database.sqlite")) { + New-Item -ItemType File -Path "database\database.sqlite" | Out-Null + Write-Host "[INFO] Created SQLite database" -ForegroundColor Cyan +} + +# Create dirs +@("storage\logs", "storage\framework\sessions", "storage\framework\views", "storage\framework\cache", + "bootstrap\cache", "public\build", "docker\nginx\ssl") | ForEach-Object { + New-Item -ItemType Directory -Force -Path $_ | Out-Null +} + +# Generate self-signed cert (requires OpenSSL in PATH) +if (-not (Test-Path "docker\nginx\ssl\cert.pem")) { + try { + & openssl req -x509 -nodes -days 365 -newkey rsa:2048 ` + -keyout "docker\nginx\ssl\key.pem" ` + -out "docker\nginx\ssl\cert.pem" ` + -subj "/CN=localhost" 2>&1 | Out-Null + Write-Host "[OK] SSL certificate generated" -ForegroundColor Green + } catch { + Write-Host "[WARN] Could not generate SSL cert. HTTPS will not work." -ForegroundColor Yellow + } +} + +# Build and start +Write-Host "[INFO] Building and starting Docker containers..." -ForegroundColor Cyan +docker compose build php +docker compose up -d nginx redis +Start-Sleep -Seconds 8 + +# Laravel setup +Write-Host "[INFO] Running Laravel setup..." -ForegroundColor Cyan +docker compose exec php composer install --optimize-autoloader --no-interaction +docker compose exec php php artisan key:generate --force +docker compose exec php php artisan migrate --force +docker compose exec php php artisan db:seed --force +docker compose exec php php artisan storage:link +docker compose exec php php artisan config:cache +docker compose exec php php artisan route:cache + +Write-Host "" +Write-Host "╔════════════════════════════════════════════════╗" -ForegroundColor Green +Write-Host "║ Infra Manager is ready! ║" -ForegroundColor Green +Write-Host "║ ║" -ForegroundColor Green +Write-Host "║ URL: http://localhost:8080 ║" -ForegroundColor Green +Write-Host "║ HTTPS: https://localhost:8443 ║" -ForegroundColor Green +Write-Host "║ Email: admin@infra.local ║" -ForegroundColor Green +Write-Host "║ Password: Admin@123456 ║" -ForegroundColor Green +Write-Host "╚════════════════════════════════════════════════╝" -ForegroundColor Green +Write-Host "" diff --git a/setup.sh b/setup.sh new file mode 100644 index 0000000..6e49c90 --- /dev/null +++ b/setup.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash +set -e + +GREEN='\033[0;32m' +CYAN='\033[0;36m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +info() { echo -e "${CYAN}[INFO]${NC} $1"; } +success() { echo -e "${GREEN}[OK]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } + +echo "" +echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" +echo -e "${CYAN}║ Infra Manager — Setup Script ║${NC}" +echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" +echo "" + +# Check Docker +command -v docker >/dev/null 2>&1 || error "Docker is not installed. Please install Docker first." +command -v docker compose >/dev/null 2>&1 || error "Docker Compose is not installed." + +success "Docker found: $(docker --version)" + +# Copy .env +if [ ! -f .env ]; then + cp .env.example .env + info "Created .env from .env.example" +else + warn ".env already exists — skipping copy" +fi + +# Create SQLite database +mkdir -p database +if [ ! -f database/database.sqlite ]; then + touch database/database.sqlite + info "Created SQLite database" +fi + +# Create required directories +mkdir -p storage/logs storage/framework/sessions storage/framework/views storage/framework/cache +mkdir -p bootstrap/cache public/build +chmod -R 775 storage bootstrap/cache +info "Created storage directories" + +# Generate SSL self-signed cert for HTTPS +mkdir -p docker/nginx/ssl +if [ ! -f docker/nginx/ssl/cert.pem ]; then + info "Generating self-signed SSL certificate..." + openssl req -x509 -nodes -days 365 -newkey rsa:2048 \ + -keyout docker/nginx/ssl/key.pem \ + -out docker/nginx/ssl/cert.pem \ + -subj "/C=US/ST=Local/L=Local/O=InfraManager/CN=localhost" 2>/dev/null + success "SSL certificate generated" +fi + +# Build and start containers +info "Building Docker containers (this may take a few minutes)..." +docker compose build --no-cache php + +info "Starting services..." +docker compose up -d nginx redis + +info "Waiting for services to be healthy..." +sleep 5 + +# Run Laravel setup inside container +info "Installing PHP dependencies..." +docker compose exec php composer install --optimize-autoloader --no-interaction + +info "Generating application key..." +docker compose exec php php artisan key:generate --force + +info "Running database migrations..." +docker compose exec php php artisan migrate --force + +info "Seeding database (default admin user)..." +docker compose exec php php artisan db:seed --force + +info "Creating storage link..." +docker compose exec php php artisan storage:link + +info "Caching routes and config..." +docker compose exec php php artisan config:cache +docker compose exec php php artisan route:cache + +success "Setup complete!" +echo "" +echo -e "${GREEN}╔════════════════════════════════════════════════╗${NC}" +echo -e "${GREEN}║ Infra Manager is ready! ║${NC}" +echo -e "${GREEN}║ ║${NC}" +echo -e "${GREEN}║ URL: http://localhost:8080 ║${NC}" +echo -e "${GREEN}║ HTTPS: https://localhost:8443 ║${NC}" +echo -e "${GREEN}║ Email: admin@infra.local ║${NC}" +echo -e "${GREEN}║ Password: Admin@123456 ║${NC}" +echo -e "${GREEN}║ ║${NC}" +echo -e "${GREEN}║ Run dev mode (with HMR): ║${NC}" +echo -e "${GREEN}║ docker compose --profile dev up ║${NC}" +echo -e "${GREEN}╚════════════════════════════════════════════════╝${NC}" +echo "" diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..f0851bc --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,149 @@ +const { fontFamily } = require("tailwindcss/defaultTheme"); + +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: [ + "./resources/js/**/*.{js,jsx,ts,tsx}", + "./resources/views/**/*.blade.php", + ], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + // Custom infra-manager colors + cyber: { + 50: "#f0fdf9", + 100: "#ccfbef", + 200: "#99f6df", + 300: "#5eead4", + 400: "#2dd4bf", + 500: "#14b8a6", + 600: "#0d9488", + 700: "#0f766e", + 800: "#115e59", + 900: "#134e4a", + 950: "#042f2e", + }, + neon: { + blue: "#00d4ff", + green: "#00ff88", + purple: "#9d4edd", + orange: "#ff6b35", + red: "#ff2d55", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + fontFamily: { + sans: ["Inter", ...fontFamily.sans], + mono: ["JetBrains Mono", ...fontFamily.mono], + }, + keyframes: { + "accordion-down": { + from: { height: 0 }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: 0 }, + }, + shimmer: { + from: { backgroundPosition: "0 0" }, + to: { backgroundPosition: "-200% 0" }, + }, + "border-beam": { + "100%": { "offset-distance": "100%" }, + }, + aurora: { + from: { backgroundPosition: "50% 50%, 50% 50%" }, + to: { backgroundPosition: "350% 50%, 350% 50%" }, + }, + meteor: { + "0%": { transform: "rotate(215deg) translateX(0)", opacity: 1 }, + "70%": { opacity: 1 }, + "100%": { + transform: "rotate(215deg) translateX(-500px)", + opacity: 0, + }, + }, + grid: { + "0%": { transform: "translateY(-50%)" }, + "100%": { transform: "translateY(0)" }, + }, + pulse: { + "0%, 100%": { boxShadow: "0 0 0 0 var(--pulse-color)" }, + "50%": { boxShadow: "0 0 0 8px var(--pulse-color)" }, + }, + "spin-around": { + "0%": { transform: "translateZ(0) rotate(0)" }, + "15%, 35%": { transform: "translateZ(0) rotate(90deg)" }, + "65%, 85%": { transform: "translateZ(0) rotate(270deg)" }, + "100%": { transform: "translateZ(0) rotate(360deg)" }, + }, + slide: { + to: { transform: "translate(calc(100cqw - 100%), 0)" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + shimmer: "shimmer 2s linear infinite", + "border-beam": "border-beam calc(var(--duration)*1s) infinite linear", + aurora: "aurora 60s linear infinite", + meteor: "meteor 5s linear infinite", + grid: "grid 15s linear infinite", + pulse: "pulse var(--duration) ease-out infinite", + "spin-around": "spin-around calc(var(--speed) * 2) infinite linear", + slide: "slide var(--speed) ease-in-out infinite alternate", + }, + }, + }, + plugins: [ + require("tailwindcss-animate"), + require("@tailwindcss/forms"), + require("@tailwindcss/typography"), + ], +}; diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..f680c5c --- /dev/null +++ b/vite.config.js @@ -0,0 +1,29 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; +import { resolve } from 'path'; + +export default defineConfig({ + plugins: [ + react(), + ], + resolve: { + alias: { + '@': resolve(__dirname, 'resources/js'), + '~': resolve(__dirname, 'resources'), + }, + }, + build: { + outDir: 'public/build', + manifest: true, + rollupOptions: { + input: 'resources/js/app.jsx', + }, + }, + server: { + host: '0.0.0.0', + port: 5173, + hmr: { + host: 'localhost', + }, + }, +});