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) <noreply@anthropic.com>
feat/v1-foundation
boban 2026-06-12 06:25:47 +02:00
parent 47eae61371
commit 506fe4044a
8 changed files with 250 additions and 0 deletions

34
app/Models/AuditEvent.php Normal file
View File

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class AuditEvent extends Model
{
protected $guarded = [];
protected $casts = ['meta' => '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);
}
}

39
app/Models/Server.php Normal file
View File

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Support\Str;
class Server extends Model
{
protected $guarded = [];
protected $casts = [
'specs' => '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);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class SshCredential extends Model
{
protected $guarded = [];
// Hidden from serialization; encrypted at rest via APP_KEY.
protected $hidden = ['secret', 'passphrase'];
protected $casts = [
'secret' => '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);
}
}

View File

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('servers', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('audit_events', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('ssh_credentials', function (Blueprint $table) {
$table->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');
}
};

View File

@ -0,0 +1,45 @@
<?php
namespace Database\Seeders;
use App\Models\AuditEvent;
use App\Models\Server;
use Illuminate\Database\Seeder;
class FleetSeeder extends Seeder
{
public function run(): void
{
$fleet = [
['name' => '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,
);
}
}
}

View File

@ -1,6 +1,17 @@
<?php
use App\Livewire\Audit;
use App\Livewire\Dashboard;
use App\Livewire\Files;
use App\Livewire\Servers;
use App\Livewire\Services;
use Illuminate\Support\Facades\Route;
Route::get('/', Dashboard::class)->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');