143 lines
5.3 KiB
PHP
143 lines
5.3 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Modals;
|
|
|
|
use App\Models\AuditEvent;
|
|
use App\Models\Server;
|
|
use App\Services\FleetService;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\Rule;
|
|
use Illuminate\Validation\ValidationException;
|
|
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(__('modals.create_server.validation_ip_or_host'));
|
|
}
|
|
}],
|
|
'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' => __('modals.create_server.attr_name'),
|
|
'ip' => __('modals.create_server.attr_ip'),
|
|
'sshPort' => __('modals.create_server.attr_ssh_port'),
|
|
'username' => __('modals.create_server.attr_user'),
|
|
'authType' => __('modals.create_server.attr_auth'),
|
|
'secret' => $this->authType === 'key' ? __('modals.create_server.attr_secret_key') : __('modals.create_server.attr_secret_password'),
|
|
'credentialName' => __('modals.create_server.attr_credential_name'),
|
|
];
|
|
}
|
|
|
|
public function save(): void
|
|
{
|
|
$data = $this->validate();
|
|
|
|
// Atomic: create server + credential, then VERIFY the SSH login. A failed
|
|
// probe throws -> the whole transaction rolls back, so no half-registered
|
|
// server is left behind, and the operator sees the real SSH reason on the
|
|
// form. A verified server starts as 'pending' ("Initialisierung") — never
|
|
// red — until the first contact promotes it.
|
|
$server = DB::transaction(function () use ($data) {
|
|
$server = Server::create([
|
|
'name' => trim($data['name']),
|
|
'ip' => trim($data['ip']),
|
|
'ssh_port' => (int) $data['sshPort'],
|
|
'status' => 'pending',
|
|
]);
|
|
|
|
$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,
|
|
]);
|
|
|
|
$server->load('credential');
|
|
$probe = app(FleetService::class)->testConnection($server);
|
|
if (! $probe['ok']) {
|
|
throw ValidationException::withMessages([
|
|
'secret' => __('modals.create_server.validation_ssh_failed', ['error' => Str::limit((string) $probe['error'], 120)]),
|
|
]);
|
|
}
|
|
|
|
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: __('modals.create_server.notify_added'));
|
|
$this->closeModal();
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
return view('livewire.modals.create-server');
|
|
}
|
|
}
|