CluPilotCloud/app/Livewire/Admin/EditExportTarget.php

139 lines
4.8 KiB
PHP

<?php
namespace App\Livewire\Admin;
use App\Models\ExportTarget;
use App\Services\Secrets\SecretCipher;
use LivewireUI\Modal\ModalComponent;
/**
* Adding or changing one destination.
*
* The password never lives here. It goes into the secret vault under a key
* derived from the target, and only that key is stored on the row — a
* credential in a settings table is a credential in every database dump. An
* empty password field on an existing target means "leave it alone", not
* "clear it": the field cannot show what is stored, so treating blank as a
* deletion would wipe the credential every time somebody renamed the target.
*/
class EditExportTarget extends ModalComponent
{
public string $uuid = '';
public string $name = '';
public string $driver = ExportTarget::LOCAL;
public string $path = '';
/** 'year' for an archive somebody keeps, 'day' for a handover point. */
public string $layout = ExportTarget::BY_YEAR;
/** Empty means keep for ever, which is the default and the safe one. */
public ?int $keepDays = null;
public string $host = '';
public ?int $port = 22;
public string $username = '';
public string $password = '';
public bool $hasPassword = false;
public bool $active = true;
public function mount(?string $uuid = null): void
{
$this->authorize('site.manage'); // modals bypass the route middleware
if ($uuid === null) {
return;
}
$target = ExportTarget::query()->where('uuid', $uuid)->firstOrFail();
$this->uuid = $uuid;
$this->name = $target->name;
$this->driver = $target->driver;
$this->path = $target->path;
$this->layout = $target->layout;
$this->keepDays = $target->keep_days;
$this->host = (string) $target->host;
$this->port = $target->port ?: 22;
$this->username = (string) $target->username;
$this->active = (bool) $target->active;
$this->hasPassword = trim((string) $target->secret_key) !== '';
}
public function save()
{
$this->authorize('site.manage');
$rules = [
'name' => 'required|string|max:120',
'driver' => 'required|in:local,sftp',
'path' => 'required|string|max:255',
'layout' => 'required|in:year,day',
// Nothing, or a real number of days. Zero would read as "delete
// immediately" to somebody who typed it meaning "never".
'keepDays' => 'nullable|integer|min:1|max:3650',
'active' => 'boolean',
];
if ($this->driver === ExportTarget::SFTP) {
$rules += [
'host' => 'required|string|max:190',
'port' => 'required|integer|min:1|max:65535',
'username' => 'required|string|max:120',
// Required only when there is nothing stored yet. Blank on an
// existing target means "unchanged" — the field cannot show
// what is stored, so treating blank as a deletion would wipe
// the credential on every unrelated edit.
'password' => $this->hasPassword ? 'nullable|string|max:255' : 'required|string|max:255',
];
}
$data = $this->validate($rules);
$target = $this->uuid !== ''
? ExportTarget::query()->where('uuid', $this->uuid)->firstOrFail()
: new ExportTarget;
$target->fill([
'name' => $data['name'],
'driver' => $data['driver'],
'path' => rtrim($data['path'], '/') ?: '/',
'layout' => $data['layout'],
'keep_days' => $data['keepDays'] ?: null,
'host' => $this->driver === ExportTarget::SFTP ? $this->host : null,
'port' => $this->driver === ExportTarget::SFTP ? $this->port : null,
'username' => $this->driver === ExportTarget::SFTP ? $this->username : null,
'active' => $data['active'] ?? true,
]);
$target->save();
// Encrypted onto the row rather than into the vault. The vault keeps a
// registry of fixed, documented credentials and refuses anything else,
// which is right for it — a per-destination password is not one of
// those, and forcing it in would make the registry meaningless. Same
// cipher, same key, so it is no more readable in a dump than the rest.
if ($this->driver === ExportTarget::SFTP && $this->password !== '') {
$target->forceFill([
'secret_key' => app(SecretCipher::class)->encrypt($this->password),
])->save();
}
$this->dispatch('notify', message: __('finance.targets_saved'));
return $this->redirectRoute('admin.finance', navigate: true);
}
public function render()
{
return view('livewire.admin.edit-export-target');
}
}