From 506fe4044a8f19da3bcab6e8f68dc86d7490cfa3 Mon Sep 17 00:00:00 2001 From: boban Date: Fri, 12 Jun 2026 06:25:47 +0200 Subject: [PATCH] feat: v1 data models + routes (Server/AuditEvent/SshCredential) - Server (uuid route key R11; cpu/mem/disk/specs/status/uptime), AuditEvent (actor/action/target + user/server relations), SshCredential (encrypted vault: secret/passphrase cast as 'encrypted', hidden). Migrations + FleetSeeder (4 servers, 4 events). - routes/web.php: full-page Livewire routes for dashboard, servers (index + {server:uuid} show), services, files, audit. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/Models/AuditEvent.php | 34 ++++++++++++++ app/Models/Server.php | 39 ++++++++++++++++ app/Models/SshCredential.php | 30 +++++++++++++ ...2026_06_12_041823_create_servers_table.php | 34 ++++++++++++++ ...06_12_041824_create_audit_events_table.php | 30 +++++++++++++ ...12_041824_create_ssh_credentials_table.php | 27 +++++++++++ database/seeders/FleetSeeder.php | 45 +++++++++++++++++++ routes/web.php | 11 +++++ 8 files changed, 250 insertions(+) create mode 100644 app/Models/AuditEvent.php create mode 100644 app/Models/Server.php create mode 100644 app/Models/SshCredential.php create mode 100644 database/migrations/2026_06_12_041823_create_servers_table.php create mode 100644 database/migrations/2026_06_12_041824_create_audit_events_table.php create mode 100644 database/migrations/2026_06_12_041824_create_ssh_credentials_table.php create mode 100644 database/seeders/FleetSeeder.php diff --git a/app/Models/AuditEvent.php b/app/Models/AuditEvent.php new file mode 100644 index 0000000..c83f862 --- /dev/null +++ b/app/Models/AuditEvent.php @@ -0,0 +1,34 @@ + 'array']; + + protected static function booted(): void + { + static::creating(fn (self $e) => $e->uuid ??= (string) Str::uuid()); + } + + public function getRouteKeyName(): string + { + return 'uuid'; + } + + public function user(): BelongsTo + { + return $this->belongsTo(User::class); + } + + public function server(): BelongsTo + { + return $this->belongsTo(Server::class); + } +} diff --git a/app/Models/Server.php b/app/Models/Server.php new file mode 100644 index 0000000..4a8771e --- /dev/null +++ b/app/Models/Server.php @@ -0,0 +1,39 @@ + 'array', + 'last_seen_at' => 'datetime', + ]; + + protected static function booted(): void + { + static::creating(fn (self $s) => $s->uuid ??= (string) Str::uuid()); + } + + // R11: URLs address servers by uuid, never the bigint id. + public function getRouteKeyName(): string + { + return 'uuid'; + } + + public function credential(): HasOne + { + return $this->hasOne(SshCredential::class); + } + + public function auditEvents(): HasMany + { + return $this->hasMany(AuditEvent::class); + } +} diff --git a/app/Models/SshCredential.php b/app/Models/SshCredential.php new file mode 100644 index 0000000..27ea1fc --- /dev/null +++ b/app/Models/SshCredential.php @@ -0,0 +1,30 @@ + 'encrypted', + 'passphrase' => 'encrypted', + ]; + + protected static function booted(): void + { + static::creating(fn (self $c) => $c->uuid ??= (string) Str::uuid()); + } + + public function server(): BelongsTo + { + return $this->belongsTo(Server::class); + } +} diff --git a/database/migrations/2026_06_12_041823_create_servers_table.php b/database/migrations/2026_06_12_041823_create_servers_table.php new file mode 100644 index 0000000..ef46d08 --- /dev/null +++ b/database/migrations/2026_06_12_041823_create_servers_table.php @@ -0,0 +1,34 @@ +id(); + $table->uuid('uuid')->unique(); + $table->string('name'); + $table->string('hostname')->nullable(); + $table->string('ip'); + $table->unsignedSmallInteger('ssh_port')->default(22); + $table->string('os')->nullable(); + $table->string('status')->default('offline'); // online|warning|offline + $table->unsignedTinyInteger('cpu')->default(0); + $table->unsignedTinyInteger('mem')->default(0); + $table->unsignedTinyInteger('disk')->default(0); + $table->string('uptime')->nullable(); + $table->json('specs')->nullable(); + $table->timestamp('last_seen_at')->nullable(); + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('servers'); + } +}; diff --git a/database/migrations/2026_06_12_041824_create_audit_events_table.php b/database/migrations/2026_06_12_041824_create_audit_events_table.php new file mode 100644 index 0000000..9ac27c4 --- /dev/null +++ b/database/migrations/2026_06_12_041824_create_audit_events_table.php @@ -0,0 +1,30 @@ +id(); + $table->uuid('uuid')->unique(); + $table->foreignId('user_id')->nullable()->constrained()->nullOnDelete(); + $table->foreignId('server_id')->nullable()->constrained()->nullOnDelete(); + $table->string('actor'); // username or 'system' + $table->string('action'); + $table->string('target')->nullable(); + $table->string('ip', 45)->nullable(); + $table->json('meta')->nullable(); + $table->timestamps(); + $table->index('created_at'); + }); + } + + public function down(): void + { + Schema::dropIfExists('audit_events'); + } +}; diff --git a/database/migrations/2026_06_12_041824_create_ssh_credentials_table.php b/database/migrations/2026_06_12_041824_create_ssh_credentials_table.php new file mode 100644 index 0000000..f35d01b --- /dev/null +++ b/database/migrations/2026_06_12_041824_create_ssh_credentials_table.php @@ -0,0 +1,27 @@ +id(); + $table->uuid('uuid')->unique(); + $table->foreignId('server_id')->constrained()->cascadeOnDelete(); + $table->string('username'); + $table->string('auth_type')->default('key'); // key|password + $table->text('secret'); // encrypted (private key or password) + $table->text('passphrase')->nullable(); // encrypted + $table->timestamps(); + }); + } + + public function down(): void + { + Schema::dropIfExists('ssh_credentials'); + } +}; diff --git a/database/seeders/FleetSeeder.php b/database/seeders/FleetSeeder.php new file mode 100644 index 0000000..e4e0ab0 --- /dev/null +++ b/database/seeders/FleetSeeder.php @@ -0,0 +1,45 @@ + 'web-01', 'ip' => '10.10.90.136', 'os' => 'Debian 13', 'status' => 'online', 'cpu' => 34, 'mem' => 61, 'disk' => 71, 'uptime' => '42d'], + ['name' => 'db-02', 'ip' => '10.10.90.140', 'os' => 'Debian 12', 'status' => 'warning', 'cpu' => 78, 'mem' => 83, 'disk' => 64, 'uptime' => '11d'], + ['name' => 'cache-03', 'ip' => '10.10.90.141', 'os' => 'Alpine 3.20', 'status' => 'offline', 'cpu' => 0, 'mem' => 0, 'disk' => 0, 'uptime' => null], + ['name' => 'edge-04', 'ip' => '10.10.90.150', 'os' => 'Ubuntu 24.04', 'status' => 'online', 'cpu' => 12, 'mem' => 39, 'disk' => 28, 'uptime' => '7d'], + ]; + + foreach ($fleet as $s) { + Server::updateOrCreate(['name' => $s['name']], [ + ...$s, + 'hostname' => $s['name'].'.clusev.local', + 'specs' => ['cores' => 4, 'ram_gb' => 8, 'disk_gb' => 160, 'arch' => 'x86_64', 'kernel' => '6.12.0', 'virt' => 'KVM'], + 'last_seen_at' => $s['status'] === 'offline' ? now()->subHour() : now(), + ]); + } + + $web = Server::where('name', 'web-01')->first(); + + $events = [ + ['actor' => 'admin', 'action' => 'Dienst neu gestartet', 'target' => 'php8.3-fpm.service', 'server_id' => $web?->id, 'created_at' => now()->subMinutes(2)], + ['actor' => 'admin', 'action' => 'SSH-Schlüssel hinzugefügt', 'target' => 'web-01', 'server_id' => $web?->id, 'created_at' => now()->subMinutes(18)], + ['actor' => 'system', 'action' => 'Server offline', 'target' => 'cache-03', 'server_id' => null, 'created_at' => now()->subHour()], + ['actor' => 'admin', 'action' => 'Datei bearbeitet', 'target' => '/etc/nginx/nginx.conf', 'server_id' => $web?->id, 'created_at' => now()->subHours(3)], + ]; + + foreach ($events as $e) { + AuditEvent::updateOrCreate( + ['actor' => $e['actor'], 'action' => $e['action'], 'target' => $e['target']], + $e, + ); + } + } +} diff --git a/routes/web.php b/routes/web.php index bdeecfd..208cf03 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,6 +1,17 @@ name('dashboard'); + +Route::get('/servers', Servers\Index::class)->name('servers.index'); +Route::get('/servers/{server}', Servers\Show::class)->name('servers.show'); + +Route::get('/services', Services\Index::class)->name('services.index'); +Route::get('/files', Files\Index::class)->name('files.index'); +Route::get('/audit', Audit\Index::class)->name('audit.index');