clusev/app/Livewire/Modals/CreateServer.php

129 lines
4.4 KiB
PHP

<?php
namespace App\Livewire\Modals;
use App\Models\AuditEvent;
use App\Models\Server;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use LivewireUI\Modal\ModalComponent;
/**
* Form modal: register a new server in the fleet and deposit its SSH access in
* the encrypted vault. The server starts as 'offline'; the first poll/connect
* promotes it. The SSH port lives on the servers table (column ssh_port), which
* is what the SSH layer (SshClient/Sftp) reads.
*/
class CreateServer extends ModalComponent
{
public string $name = '';
public string $ip = '';
public int $sshPort = 22;
public string $username = '';
public string $authType = 'password';
public string $secret = '';
public string $passphrase = '';
public string $credentialName = '';
public static function modalMaxWidth(): string
{
return 'lg';
}
/**
* @return array<string, array<int, mixed>>
*/
protected function rules(): array
{
return [
'name' => ['required', 'string', 'max:120'],
// Accept either a real IP (validated by PHP's filter) or a valid DNS hostname.
'ip' => ['required', 'string', 'max:255', function ($attribute, $value, $fail) {
$value = (string) $value;
$isIp = filter_var($value, FILTER_VALIDATE_IP) !== false;
// A digits-and-dots string is an IPv4 attempt — it must be a VALID IP,
// not a numeric "hostname" (so 999.999.999.999 is rejected).
$numericDotted = preg_match('/^[0-9.]+$/', $value) === 1;
$isHost = ! $numericDotted && preg_match('/^(?=.{1,253}$)(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)*[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$/', $value) === 1;
if (! $isIp && ! $isHost) {
$fail('Bitte eine gültige IP-Adresse oder einen Hostnamen angeben.');
}
}],
'sshPort' => ['required', 'integer', 'min:1', 'max:65535'],
'username' => ['required', 'string', 'max:120'],
'authType' => ['required', Rule::in(['password', 'key'])],
'secret' => ['required', 'string'],
'passphrase' => ['nullable', 'string'],
'credentialName' => ['nullable', 'string', 'max:120'],
];
}
/**
* @return array<string, string>
*/
protected function validationAttributes(): array
{
return [
'name' => 'Name',
'ip' => 'IP/Host',
'sshPort' => 'SSH-Port',
'username' => 'Benutzer',
'authType' => 'Authentifizierung',
'secret' => $this->authType === 'key' ? 'Privater Schlüssel' : 'Passwort',
'credentialName' => 'Zugangs-Name',
];
}
public function save(): void
{
$data = $this->validate();
// Atomic: a failure creating the credential or audit must not leave a
// server row without its required SSH credential.
$server = DB::transaction(function () use ($data) {
$server = Server::create([
'name' => trim($data['name']),
'ip' => trim($data['ip']),
'ssh_port' => (int) $data['sshPort'],
'status' => 'offline',
]);
$server->credential()->create([
'name' => trim($this->credentialName) !== '' ? trim($this->credentialName) : null,
'username' => trim($data['username']),
'auth_type' => $data['authType'] === 'key' ? 'key' : 'password',
'secret' => $this->secret,
'passphrase' => $this->authType === 'key' && $this->passphrase !== '' ? $this->passphrase : null,
]);
AuditEvent::create([
'user_id' => Auth::id(),
'server_id' => $server->id,
'actor' => Auth::user()?->name ?? 'system',
'action' => 'server.create',
'target' => $server->name.' · '.$server->ip,
'ip' => request()->ip(),
]);
return $server;
});
$this->dispatch('serverCreated');
$this->dispatch('notify', message: 'Server hinzugefügt.');
$this->closeModal();
}
public function render()
{
return view('livewire.modals.create-server');
}
}