diff --git a/app/Livewire/Files/Index.php b/app/Livewire/Files/Index.php index 6b35f90..891964e 100644 --- a/app/Livewire/Files/Index.php +++ b/app/Livewire/Files/Index.php @@ -3,6 +3,7 @@ namespace App\Livewire\Files; use Livewire\Attributes\Layout; +use Livewire\Attributes\On; use Livewire\Attributes\Title; use Livewire\Component; @@ -52,6 +53,39 @@ class Index extends Component return $crumbs; } + /** + * Delete a file (R5): opens the confirm modal, which writes the AuditEvent + * and re-dispatches `fileConfirmed`. SFTP unlink is wired in behind this later. + */ + public function confirmDelete(string $name): void + { + $this->dispatch('openModal', + component: 'modals.confirm-action', + arguments: [ + 'heading' => 'Datei löschen', + 'body' => "„{$name}“ wird unwiderruflich aus {$this->path} entfernt.", + 'confirmLabel' => 'Löschen', + 'danger' => true, + 'icon' => 'trash', + 'auditAction' => 'file.delete', + 'auditTarget' => rtrim($this->path, '/')."/{$name}", + 'event' => 'fileConfirmed', + 'params' => ['name' => $name], + 'notify' => "„{$name}“ gelöscht.", + ], + ); + } + + /** Applies the confirmed deletion (mock until SFTP unlink lands). */ + #[On('fileConfirmed')] + public function deleteEntry(string $name): void + { + $this->entries = array_values(array_filter( + $this->entries, + fn (array $e): bool => $e['name'] !== $name, + )); + } + public function render() { return view('livewire.files.index'); diff --git a/app/Livewire/Modals/ConfirmAction.php b/app/Livewire/Modals/ConfirmAction.php new file mode 100644 index 0000000..4c617f0 --- /dev/null +++ b/app/Livewire/Modals/ConfirmAction.php @@ -0,0 +1,75 @@ + */ + public array $params = []; + + public string $notify = 'Aktion ausgeführt.'; + + public static function modalMaxWidth(): string + { + return 'md'; + } + + public function confirm(): void + { + if ($this->auditAction !== '') { + AuditEvent::create([ + 'user_id' => Auth::id(), + 'server_id' => $this->serverId, + 'actor' => Auth::user()?->name ?? 'system', + 'action' => $this->auditAction, + 'target' => $this->auditTarget, + 'ip' => request()->ip(), + ]); + } + + if ($this->event !== '') { + $this->dispatch($this->event, ...$this->params); + } + + $this->dispatch('notify', message: $this->notify); + + $this->closeModal(); + } + + public function render() + { + return view('livewire.modals.confirm-action'); + } +} diff --git a/app/Livewire/Servers/Show.php b/app/Livewire/Servers/Show.php index c8464d5..a80b527 100644 --- a/app/Livewire/Servers/Show.php +++ b/app/Livewire/Servers/Show.php @@ -4,6 +4,7 @@ namespace App\Livewire\Servers; use App\Models\Server; use Livewire\Attributes\Layout; +use Livewire\Attributes\On; use Livewire\Attributes\Title; use Livewire\Component; @@ -57,6 +58,40 @@ class Show extends Component ]; } + /** + * Revoke an SSH key (R5): opens the confirm modal, which writes the + * AuditEvent and re-dispatches `keyRemoved`. SSH layer removal lands later. + */ + public function confirmKeyRemoval(string $fingerprint, string $comment): void + { + $this->dispatch('openModal', + component: 'modals.confirm-action', + arguments: [ + 'heading' => 'SSH-Schlüssel entfernen', + 'body' => "Der Schlüssel „{$comment}“ verliert den Zugang zu {$this->server->name}.", + 'confirmLabel' => 'Entfernen', + 'danger' => true, + 'icon' => 'trash', + 'auditAction' => 'ssh_key.remove', + 'auditTarget' => "{$comment} · {$this->server->name}", + 'serverId' => $this->server->id, + 'event' => 'keyRemoved', + 'params' => ['fingerprint' => $fingerprint], + 'notify' => "Schlüssel „{$comment}“ entfernt.", + ], + ); + } + + /** Applies the confirmed key removal (mock until SSH layer lands). */ + #[On('keyRemoved')] + public function removeKey(string $fingerprint): void + { + $this->sshKeys = array_values(array_filter( + $this->sshKeys, + fn (array $k): bool => $k['fingerprint'] !== $fingerprint, + )); + } + public function render() { return view('livewire.servers.show'); diff --git a/app/Livewire/Services/Index.php b/app/Livewire/Services/Index.php index a65e9b2..11632a7 100644 --- a/app/Livewire/Services/Index.php +++ b/app/Livewire/Services/Index.php @@ -3,6 +3,7 @@ namespace App\Livewire\Services; use Livewire\Attributes\Layout; +use Livewire\Attributes\On; use Livewire\Attributes\Title; use Livewire\Component; @@ -63,22 +64,44 @@ class Index extends Component } /** - * Service control. Wired to the SSH layer (systemctl start/stop/restart) - * behind a confirmation modal later — see R5 markers in the view. + * Service control (R5): opens the wire-elements/modal confirm dialog. The + * modal writes the AuditEvent and re-dispatches `serviceConfirmed`, which + * applyService() handles. SSH exec (systemctl) is wired in behind this later. */ - public function start(string $name): void + public function confirm(string $op, string $name): void { - // R5: routed through wire-elements/modal + SSH exec later. + [$heading, $phrase, $label, $action, $icon] = match ($op) { + 'start' => ['Dienst starten', 'wird gestartet', 'Starten', 'service.start', 'power'], + 'stop' => ['Dienst stoppen', 'wird gestoppt', 'Stoppen', 'service.stop', 'power'], + default => ['Dienst neu starten', 'wird neu gestartet', 'Neu starten', 'service.restart', 'rotate'], + }; + + $this->dispatch('openModal', + component: 'modals.confirm-action', + arguments: [ + 'heading' => $heading, + 'body' => "{$name} {$phrase}. Ausgeführt über systemctl auf {$this->server}.", + 'confirmLabel' => $label, + 'danger' => $op === 'stop', + 'icon' => $icon, + 'auditAction' => $action, + 'auditTarget' => "{$name} · {$this->server}", + 'event' => 'serviceConfirmed', + 'params' => ['op' => $op, 'name' => $name], + 'notify' => "{$name}: {$label} ausgeführt.", + ], + ); } - public function stop(string $name): void + /** Applies the confirmed service state change (mock until SSH exec lands). */ + #[On('serviceConfirmed')] + public function applyService(string $op, string $name): void { - // R5: routed through wire-elements/modal + SSH exec later. - } - - public function restart(string $name): void - { - // R5: routed through wire-elements/modal + SSH exec later. + foreach ($this->services as $i => $svc) { + if ($svc['name'] === $name) { + $this->services[$i]['status'] = $op === 'stop' ? 'offline' : 'online'; + } + } } /** @return array */ diff --git a/config/wire-elements-modal.php b/config/wire-elements-modal.php new file mode 100644 index 0000000..2950291 --- /dev/null +++ b/config/wire-elements-modal.php @@ -0,0 +1,52 @@ + false, + + /* + |-------------------------------------------------------------------------- + | Include JS + |-------------------------------------------------------------------------- + | + | Livewire UI will inject the required Javascript in your blade template. + | If you want to bundle the required Javascript you can set this to false + | and add `require('vendor/wire-elements/modal/resources/js/modal');` + | to your script bundler like webpack. + | + */ + 'include_js' => true, + + /* + |-------------------------------------------------------------------------- + | Modal Component Defaults + |-------------------------------------------------------------------------- + | + | Configure the default properties for a modal component. + | + | Supported modal_max_width + | 'sm', 'md', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl' + */ + 'component_defaults' => [ + 'modal_max_width' => '2xl', + + 'close_modal_on_click_away' => true, + + 'close_modal_on_escape' => true, + + 'close_modal_on_escape_is_forceful' => true, + + 'dispatch_close_event' => false, + + 'destroy_on_close' => false, + ], +]; diff --git a/resources/views/components/icon.blade.php b/resources/views/components/icon.blade.php index 6f7ba0e..eea1d2f 100644 --- a/resources/views/components/icon.blade.php +++ b/resources/views/components/icon.blade.php @@ -16,6 +16,10 @@ 'shield' => '', 'plus' => '', 'logout' => '', + 'alert' => '', + 'power' => '', + 'rotate' => '', + 'trash' => '', ]; @endphp merge(['class' => $class]) }} viewBox="0 0 24 24" fill="none" stroke="currentColor" diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 2e70625..d2a309e 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -25,6 +25,30 @@ + {{-- Toaster: catches Livewire-dispatched `notify` browser events --}} +
+ +
+ + @livewireScripts diff --git a/resources/views/livewire/files/index.blade.php b/resources/views/livewire/files/index.blade.php index 3f4f0a4..ebf5fe2 100644 --- a/resources/views/livewire/files/index.blade.php +++ b/resources/views/livewire/files/index.blade.php @@ -118,8 +118,8 @@ - {{-- R5: wire to wire-elements/modal --}} - diff --git a/resources/views/livewire/modals/confirm-action.blade.php b/resources/views/livewire/modals/confirm-action.blade.php new file mode 100644 index 0000000..4c90045 --- /dev/null +++ b/resources/views/livewire/modals/confirm-action.blade.php @@ -0,0 +1,39 @@ +
+
+ $danger, + 'border-accent/30 bg-accent/10 text-accent-text' => ! $danger, + ])> + + +
+

{{ $heading }}

+

{{ $body }}

+ @if ($auditTarget) +

{{ $auditTarget }}

+ @endif +
+
+ +
+ + +
+
diff --git a/resources/views/livewire/servers/show.blade.php b/resources/views/livewire/servers/show.blade.php index 6ded799..f97eb62 100644 --- a/resources/views/livewire/servers/show.blade.php +++ b/resources/views/livewire/servers/show.blade.php @@ -161,6 +161,13 @@

{{ $key['fingerprint'] }}

+ {{-- R5: destructive → wire-elements/modal confirm + audit --}} + @endforeach diff --git a/resources/views/livewire/services/index.blade.php b/resources/views/livewire/services/index.blade.php index 5149c15..95c4686 100644 --- a/resources/views/livewire/services/index.blade.php +++ b/resources/views/livewire/services/index.blade.php @@ -63,19 +63,19 @@
diff --git a/resources/views/vendor/wire-elements-modal/modal.blade.php b/resources/views/vendor/wire-elements-modal/modal.blade.php new file mode 100644 index 0000000..a7a20a2 --- /dev/null +++ b/resources/views/vendor/wire-elements-modal/modal.blade.php @@ -0,0 +1,57 @@ +
+ @isset($jsPath) + + @endisset + @isset($cssPath) + + @endisset + + +