> */ 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 */ 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'); } }