diff --git a/app/Livewire/Modals/FileEditor.php b/app/Livewire/Modals/FileEditor.php index 3fec574..c64b47d 100644 --- a/app/Livewire/Modals/FileEditor.php +++ b/app/Livewire/Modals/FileEditor.php @@ -5,6 +5,7 @@ namespace App\Livewire\Modals; use App\Models\AuditEvent; use App\Models\Server; use App\Services\FleetService; +use App\Support\Ssh\Sftp; use Illuminate\Support\Facades\Auth; use LivewireUI\Modal\ModalComponent; use Throwable; @@ -12,9 +13,16 @@ use Throwable; /** * View / edit a remote text file over SFTP. Loads lazily (wire:init), refuses * binary or oversized files, and writes back on save with an AuditEvent. + * Image files are shown as a read-only base64 preview instead of the editor. */ class FileEditor extends ModalComponent { + /** Image preview is capped — larger images are refused (base64 inflates ~33%). */ + private const IMAGE_MAX_BYTES = 5_242_880; // 5 MB + + /** Extensions rendered as an inline image preview (read-only). */ + private const IMAGE_EXTENSIONS = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp', 'ico', 'avif']; + public int $serverId; public string $path; @@ -31,6 +39,9 @@ class FileEditor extends ModalComponent public ?string $error = null; + /** base64 `data:` URI for image previews; null for text files. */ + public ?string $dataUri = null; + public function mount(int $serverId, string $path, string $name): void { $this->serverId = $serverId; @@ -43,7 +54,29 @@ class FileEditor extends ModalComponent return 'lg'; } - public function load(FleetService $fleet): void + /** True when the opened file is an image (preview, not editable). */ + public function isImage(): bool + { + return in_array($this->extension(), self::IMAGE_EXTENSIONS, true); + } + + private function extension(): string + { + return strtolower(pathinfo($this->name, PATHINFO_EXTENSION)); + } + + /** MIME type for the image preview data URI, keyed off the extension. */ + private function imageMime(): string + { + return match ($this->extension()) { + 'svg' => 'image/svg+xml', + 'jpg', 'jpeg' => 'image/jpeg', + 'ico' => 'image/x-icon', + default => 'image/'.$this->extension(), + }; + } + + public function load(FleetService $fleet, Sftp $sftp): void { $server = Server::find($this->serverId); if (! $server) { @@ -54,10 +87,14 @@ class FileEditor extends ModalComponent } try { - $r = $fleet->readFile($server, $this->path); - $this->content = $r['content']; - $this->binary = $r['binary']; - $this->tooBig = $r['tooBig']; + if ($this->isImage()) { + $this->loadImage($server, $sftp); + } else { + $r = $fleet->readFile($server, $this->path); + $this->content = $r['content']; + $this->binary = $r['binary']; + $this->tooBig = $r['tooBig']; + } } catch (Throwable $e) { $this->error = $e->getMessage(); } @@ -65,10 +102,44 @@ class FileEditor extends ModalComponent $this->loaded = true; } + /** + * Read the image bytes over SFTP and expose them as a base64 data URI. + * Refuses files larger than IMAGE_MAX_BYTES (checked before download). + */ + private function loadImage(Server $server, Sftp $sftp): void + { + $sftp->connect($server); + + try { + if ($sftp->size($this->path) > self::IMAGE_MAX_BYTES) { + $this->tooBig = true; + + return; + } + + $bytes = $sftp->get($this->path); + + if ($bytes === '') { + $this->error = __('modals.file_editor.image_read_failed'); + + return; + } + + $this->dataUri = 'data:'.$this->imageMime().';base64,'.base64_encode($bytes); + } finally { + $sftp->disconnect(); + } + } + public function save(FleetService $fleet): void { $this->error = null; + // Images are a read-only preview — never write them back. + if ($this->isImage()) { + return; + } + $server = Server::find($this->serverId); if (! $server) { $this->error = __('common.server_not_found'); diff --git a/lang/de/modals.php b/lang/de/modals.php index cb55a2e..0d95f1c 100644 --- a/lang/de/modals.php +++ b/lang/de/modals.php @@ -106,6 +106,8 @@ return [ // ── File editor ─────────────────────────────────────────────────────── 'file_editor' => [ 'too_big' => 'Datei zu groß zum Bearbeiten (über 256 KB) — bitte herunterladen.', + 'image_too_big' => 'Bild zu groß für die Vorschau (über 5 MB) — bitte herunterladen.', + 'image_read_failed' => 'Bild konnte nicht gelesen werden.', 'binary' => 'Binärdatei — kann nicht im Editor angezeigt werden.', 'notify_saved' => '„:name“ gespeichert.', ], diff --git a/lang/en/modals.php b/lang/en/modals.php index 90f0f3b..c681ca4 100644 --- a/lang/en/modals.php +++ b/lang/en/modals.php @@ -105,6 +105,8 @@ return [ // ── File editor ─────────────────────────────────────────────────────── 'file_editor' => [ 'too_big' => 'File too large to edit (over 256 KB) — please download it.', + 'image_too_big' => 'Image too large to preview (over 5 MB) — please download it.', + 'image_read_failed' => 'Image could not be read.', 'binary' => 'Binary file — cannot be shown in the editor.', 'notify_saved' => '“:name” saved.', ], diff --git a/resources/views/livewire/modals/add-ssh-key.blade.php b/resources/views/livewire/modals/add-ssh-key.blade.php index b22e4d9..d9a9de6 100644 --- a/resources/views/livewire/modals/add-ssh-key.blade.php +++ b/resources/views/livewire/modals/add-ssh-key.blade.php @@ -21,7 +21,7 @@ wire:model="publicKey" rows="3" placeholder="{{ __('modals.add_ssh_key.public_key_placeholder') }}" - class="w-full rounded-md border border-line bg-inset px-3 py-2.5 font-mono text-xs text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" + class="w-full resize-none rounded-md border border-line bg-inset px-3 py-2.5 font-mono text-xs text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none" > @if ($generatedPrivate) @@ -30,7 +30,7 @@ {{ __('modals.add_ssh_key.private_key_warning') }}

+ class="mt-2 w-full resize-none rounded-md border border-line bg-void px-3 py-2 font-mono text-[10px] leading-relaxed text-ink-2 focus:outline-none">{{ $generatedPrivate }} @endif diff --git a/resources/views/livewire/modals/create-server.blade.php b/resources/views/livewire/modals/create-server.blade.php index 37ae4fe..27ad9a1 100644 --- a/resources/views/livewire/modals/create-server.blade.php +++ b/resources/views/livewire/modals/create-server.blade.php @@ -52,7 +52,7 @@ @if ($authType === 'key') + class="w-full resize-none rounded-md border border-line bg-inset px-3 py-2.5 font-mono text-xs text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none"> @else diff --git a/resources/views/livewire/modals/edit-credential.blade.php b/resources/views/livewire/modals/edit-credential.blade.php index 2a17911..8390aec 100644 --- a/resources/views/livewire/modals/edit-credential.blade.php +++ b/resources/views/livewire/modals/edit-credential.blade.php @@ -32,7 +32,7 @@ @if ($authType === 'key') + class="w-full resize-none rounded-md border border-line bg-inset px-3 py-2.5 font-mono text-xs text-ink placeholder:text-ink-4 focus:border-accent/40 focus:outline-none"> @else diff --git a/resources/views/livewire/modals/file-editor.blade.php b/resources/views/livewire/modals/file-editor.blade.php index 237700d..8854a83 100644 --- a/resources/views/livewire/modals/file-editor.blade.php +++ b/resources/views/livewire/modals/file-editor.blade.php @@ -15,18 +15,22 @@ @elseif ($error)

{{ $error }}

@elseif ($tooBig) -

{{ __('modals.file_editor.too_big') }}

+

{{ $this->isImage() ? __('modals.file_editor.image_too_big') : __('modals.file_editor.too_big') }}

+ @elseif ($this->isImage()) +
+ {{ $name }} +
@elseif ($binary)

{{ __('modals.file_editor.binary') }}

@else + class="w-full resize-none rounded-md border border-line bg-void px-3 py-2.5 font-mono text-[11px] leading-relaxed text-ink-2 focus:border-accent/40 focus:outline-none"> @endif
{{ __('common.close') }} - @if ($loaded && ! $error && ! $tooBig && ! $binary) + @if ($loaded && ! $error && ! $tooBig && ! $binary && ! $this->isImage()) {{ __('common.save') }} diff --git a/resources/views/livewire/modals/recovery-codes.blade.php b/resources/views/livewire/modals/recovery-codes.blade.php index 12c185c..10dbfec 100644 --- a/resources/views/livewire/modals/recovery-codes.blade.php +++ b/resources/views/livewire/modals/recovery-codes.blade.php @@ -20,13 +20,15 @@ @endforeach
-
- - {{ __('auth.recovery_download') }} - - - {{ __('auth.recovery_regenerate') }} - - {{ __('auth.recovery_done') }} +
+
+ + {{ __('auth.recovery_download') }} + + + {{ __('auth.recovery_regenerate') }} + +
+ {{ __('common.close') }}
diff --git a/resources/views/livewire/modals/ssh-key-provision.blade.php b/resources/views/livewire/modals/ssh-key-provision.blade.php index e3a57e8..fe22101 100644 --- a/resources/views/livewire/modals/ssh-key-provision.blade.php +++ b/resources/views/livewire/modals/ssh-key-provision.blade.php @@ -41,7 +41,7 @@

{{ __('modals.ssh_key_provision.key_warning') }}

+ class="mt-2 w-full resize-none rounded-md border border-line bg-void px-3 py-2 font-mono text-[10px] leading-relaxed text-ink-2 focus:outline-none">{{ $privateKey }}