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
parent
47eae61371
commit
506fe4044a
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,6 +1,17 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Livewire\Audit;
|
||||||
use App\Livewire\Dashboard;
|
use App\Livewire\Dashboard;
|
||||||
|
use App\Livewire\Files;
|
||||||
|
use App\Livewire\Servers;
|
||||||
|
use App\Livewire\Services;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::get('/', Dashboard::class)->name('dashboard');
|
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');
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue