139 lines
4.8 KiB
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');
|
|
}
|
|
}
|