- Config: auth, services, logging, queue and database only repeated the
framework's own files and are gone; the others keep only the keys that
differ (app version, cache serializable_classes, session cookie name,
Markdown mail theme, the shares disk, three Octane values, Livewire's
pagination theme and payload guards).
- Email verification is removed: User never implemented MustVerifyEmail,
so it was never enforced, and SealShare has a single admin and no
registration. CreateNewUser goes with it.
- FileEncryptionService::encryptFile() and generateSalt() were only used
by tests; tests build files with encryptTestFile() in tests/Pest.php.
- The expiration options are defined once, as Share::EXPIRATIONS. "30 Days"
now lasts 30 days instead of a calendar month, and Admin settings only
save a default expiration that is one of the options.
- One-caller helpers are inlined, the uploader reads chunk responses with
XHR's responseType, and starter-kit leftovers are removed.
- Docker: PHP reads the PHP_* limits from the environment itself
(${VAR:-default} in uploads.ini); both entrypoints stop writing the ini.
docker-compose.yml shares the app and scheduler variables through one
anchor. The dev image installs gd for the screenshot publisher and fake
test images.
- Development runs in Docker only: the composer dev script, concurrently,
laravel/pail, laravel/sail, autoprefixer and the shell-quote override
are gone.
- phpunit.xml forces the test environment with <server> entries, so tests
run in the dev container no longer use its real database.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
249 lines
10 KiB
PHP
249 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Livewire\Admin;
|
|
|
|
use App\Models\Setting;
|
|
use App\Models\Share;
|
|
use App\Services\PasswordGeneratorService;
|
|
use Illuminate\Support\Facades\Hash;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\Rule;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Component;
|
|
use Livewire\WithFileUploads;
|
|
use NoNameWeb\LivewireMaterial\Concerns\Toasts;
|
|
use NoNameWeb\LivewireMaterial\Support\Scheme;
|
|
|
|
#[Layout('layouts.app')]
|
|
class AdminSettings extends Component
|
|
{
|
|
use Toasts;
|
|
use WithFileUploads;
|
|
|
|
/** The colour profile every page, mail and error page wears (config/livewire-material.php). */
|
|
public string $colorProfile = '';
|
|
|
|
public string $systemPassword = '';
|
|
|
|
public string $defaultExpiration = '';
|
|
|
|
public int $maxFileSize = 100;
|
|
|
|
public int $maxStorageQuota = 20;
|
|
|
|
public int $maxFilesPerShare = 50;
|
|
|
|
public int $maxSizePerShare = 2;
|
|
|
|
public bool $allowNeverExpire = false;
|
|
|
|
/** How the upload page offers generated share passwords: `off`, `button` or `prefill`. */
|
|
public string $passwordGeneratorMode = 'button';
|
|
|
|
/** `characters` or `passphrase`. */
|
|
public string $passwordGeneratorType = 'characters';
|
|
|
|
public int $passwordLength = 20;
|
|
|
|
/** @var list<string> */
|
|
public array $passwordCharacterSets = [];
|
|
|
|
public bool $passwordAvoidAmbiguous = true;
|
|
|
|
public int $passphraseWords = 6;
|
|
|
|
public string $passphraseSeparator = 'hyphen';
|
|
|
|
public string $siteTitle = '';
|
|
|
|
public string $siteDescription = '';
|
|
|
|
public $siteLogo;
|
|
|
|
/** Whether the "Remove the logo?" dialog is open. */
|
|
public bool $confirmingLogoRemoval = false;
|
|
|
|
/** Whether the "Remove the system password?" dialog is open. */
|
|
public bool $confirmingPasswordRemoval = false;
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->colorProfile = Scheme::profile() ?? '';
|
|
$this->defaultExpiration = Setting::get('default_expiration', '') ?? '';
|
|
$this->maxFileSize = (int) Setting::get('max_file_size', 100 * 1024 * 1024) / (1024 * 1024);
|
|
$this->maxStorageQuota = (int) Setting::get('max_storage_quota', 20 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024);
|
|
$this->maxFilesPerShare = (int) Setting::get('max_files_per_share', 50);
|
|
$this->maxSizePerShare = (int) Setting::get('max_size_per_share', 2 * 1024 * 1024 * 1024) / (1024 * 1024 * 1024);
|
|
$this->allowNeverExpire = (bool) Setting::get('allow_never_expire', false);
|
|
$this->siteTitle = Setting::get('site_title', '') ?? '';
|
|
$this->siteDescription = Setting::get('site_description', '') ?? '';
|
|
|
|
$passwordOptions = app(PasswordGeneratorService::class)->options();
|
|
$this->passwordGeneratorMode = $passwordOptions['mode'];
|
|
$this->passwordGeneratorType = $passwordOptions['type'];
|
|
$this->passwordLength = $passwordOptions['length'];
|
|
$this->passwordCharacterSets = $passwordOptions['characterSets'];
|
|
$this->passwordAvoidAmbiguous = $passwordOptions['avoidAmbiguous'];
|
|
$this->passphraseWords = $passwordOptions['words'];
|
|
$this->passphraseSeparator = $passwordOptions['separator'];
|
|
}
|
|
|
|
public function saveSettings(): void
|
|
{
|
|
$validated = $this->validate([
|
|
'colorProfile' => ['required', 'string', Rule::in(array_keys(Scheme::profiles()))],
|
|
'defaultExpiration' => ['nullable', 'string', Rule::in(array_keys(Share::EXPIRATIONS))],
|
|
'maxFileSize' => ['required', 'integer', 'min:1'],
|
|
'maxStorageQuota' => ['required', 'integer', 'min:1'],
|
|
'maxFilesPerShare' => ['required', 'integer', 'min:1'],
|
|
'maxSizePerShare' => ['required', 'integer', 'min:1'],
|
|
'siteTitle' => ['nullable', 'string', 'max:255'],
|
|
'siteDescription' => ['nullable', 'string', 'max:1000'],
|
|
'siteLogo' => ['nullable', 'file', 'mimes:png,jpg,jpeg,gif,webp', 'max:2048'],
|
|
...$this->passwordGeneratorRules(),
|
|
], [
|
|
'passwordCharacterSets.required' => __('Choose at least one kind of character.'),
|
|
]);
|
|
|
|
if ($this->systemPassword) {
|
|
Setting::set('system_password', Hash::make($this->systemPassword));
|
|
}
|
|
|
|
Setting::set('color_profile', $this->colorProfile);
|
|
Setting::set('default_expiration', $this->defaultExpiration ?: null);
|
|
Setting::set('max_file_size', $this->maxFileSize * 1024 * 1024);
|
|
Setting::set('max_storage_quota', $this->maxStorageQuota * 1024 * 1024 * 1024);
|
|
Setting::set('max_files_per_share', $this->maxFilesPerShare);
|
|
Setting::set('max_size_per_share', $this->maxSizePerShare * 1024 * 1024 * 1024);
|
|
|
|
Setting::set('allow_never_expire', $this->allowNeverExpire ? '1' : null);
|
|
Setting::set('site_title', $this->siteTitle ?: null);
|
|
Setting::set('site_description', $this->siteDescription ?: null);
|
|
|
|
$this->savePasswordGeneratorSettings($validated);
|
|
|
|
if ($this->siteLogo && is_object($this->siteLogo)) {
|
|
$existingLogo = Setting::get('site_logo');
|
|
if ($existingLogo) {
|
|
Storage::disk('public')->delete($existingLogo);
|
|
}
|
|
|
|
$path = $this->siteLogo->store('branding', 'public');
|
|
Setting::set('site_logo', $path);
|
|
$this->siteLogo = null;
|
|
}
|
|
|
|
$this->systemPassword = '';
|
|
|
|
$this->success(__('Settings saved successfully.'));
|
|
}
|
|
|
|
/**
|
|
* The generator's rules. A field the chosen mode or type hides is excluded, so it never blocks
|
|
* saving and keeps the value saved before.
|
|
*
|
|
* @return array<string, array<int, mixed>>
|
|
*/
|
|
protected function passwordGeneratorRules(): array
|
|
{
|
|
$characters = ['exclude_if:passwordGeneratorMode,off', 'exclude_unless:passwordGeneratorType,characters'];
|
|
$passphrase = ['exclude_if:passwordGeneratorMode,off', 'exclude_unless:passwordGeneratorType,passphrase'];
|
|
|
|
return [
|
|
'passwordGeneratorMode' => ['required', 'string', Rule::in(PasswordGeneratorService::MODES)],
|
|
'passwordGeneratorType' => ['exclude_if:passwordGeneratorMode,off', 'required', 'string', Rule::in(PasswordGeneratorService::TYPES)],
|
|
'passwordLength' => [...$characters, 'required', 'integer', 'min:'.PasswordGeneratorService::MIN_LENGTH, 'max:'.PasswordGeneratorService::MAX_LENGTH],
|
|
'passwordCharacterSets' => [...$characters, 'required', 'array'],
|
|
'passwordCharacterSets.*' => [...$characters, 'string', Rule::in(array_keys(PasswordGeneratorService::CHARACTER_SETS))],
|
|
'passwordAvoidAmbiguous' => [...$characters, 'boolean'],
|
|
'passphraseWords' => [...$passphrase, 'required', 'integer', 'min:'.PasswordGeneratorService::MIN_WORDS, 'max:'.PasswordGeneratorService::MAX_WORDS],
|
|
'passphraseSeparator' => [...$passphrase, 'required', 'string', Rule::in(array_keys(PasswordGeneratorService::SEPARATORS))],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Store the generator settings that passed validation; excluded ones keep their saved value.
|
|
*
|
|
* @param array<string, mixed> $validated
|
|
*/
|
|
protected function savePasswordGeneratorSettings(array $validated): void
|
|
{
|
|
Setting::set('password_generator_mode', $validated['passwordGeneratorMode']);
|
|
|
|
if (array_key_exists('passwordGeneratorType', $validated)) {
|
|
Setting::set('password_generator_type', $validated['passwordGeneratorType']);
|
|
}
|
|
|
|
if (array_key_exists('passwordLength', $validated)) {
|
|
Setting::set('password_generator_length', $validated['passwordLength']);
|
|
Setting::set('password_generator_character_sets', implode(',', $validated['passwordCharacterSets']));
|
|
Setting::set('password_generator_avoid_ambiguous', $validated['passwordAvoidAmbiguous'] ? '1' : '0');
|
|
}
|
|
|
|
if (array_key_exists('passphraseWords', $validated)) {
|
|
Setting::set('password_generator_words', $validated['passphraseWords']);
|
|
Setting::set('password_generator_separator', $validated['passphraseSeparator']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The form's generator options while they are valid, for the example; `null` otherwise.
|
|
*
|
|
* @return array{type: string, length: int, characterSets: list<string>, avoidAmbiguous: bool, words: int, separator: string}|null
|
|
*/
|
|
protected function passwordPreviewOptions(): ?array
|
|
{
|
|
$values = $this->only(['passwordGeneratorMode', 'passwordGeneratorType', 'passwordLength', 'passwordCharacterSets', 'passwordAvoidAmbiguous', 'passphraseWords', 'passphraseSeparator']);
|
|
|
|
if ($this->passwordGeneratorMode === 'off' || Validator::make($values, $this->passwordGeneratorRules())->fails()) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'type' => $this->passwordGeneratorType,
|
|
'length' => $this->passwordLength,
|
|
'characterSets' => array_values($this->passwordCharacterSets),
|
|
'avoidAmbiguous' => $this->passwordAvoidAmbiguous,
|
|
'words' => $this->passphraseWords,
|
|
'separator' => $this->passphraseSeparator,
|
|
];
|
|
}
|
|
|
|
public function removeLogo(): void
|
|
{
|
|
$existingLogo = Setting::get('site_logo');
|
|
|
|
if ($existingLogo) {
|
|
Storage::disk('public')->delete($existingLogo);
|
|
Setting::set('site_logo', null);
|
|
}
|
|
|
|
$this->confirmingLogoRemoval = false;
|
|
|
|
$this->success(__('Logo removed.'));
|
|
}
|
|
|
|
public function clearSystemPassword(): void
|
|
{
|
|
Setting::set('system_password', null);
|
|
|
|
$this->confirmingPasswordRemoval = false;
|
|
|
|
$this->success(__('System password cleared.'));
|
|
}
|
|
|
|
public function render(): mixed
|
|
{
|
|
$passwordGenerator = app(PasswordGeneratorService::class);
|
|
$passwordPreviewOptions = $this->passwordPreviewOptions();
|
|
|
|
return view('livewire.admin.admin-settings', [
|
|
'hasSystemPassword' => (bool) Setting::get('system_password'),
|
|
'currentLogo' => Setting::get('site_logo'),
|
|
'passwordExample' => $passwordPreviewOptions ? $passwordGenerator->generate($passwordPreviewOptions) : null,
|
|
'passwordEntropy' => $passwordPreviewOptions ? $passwordGenerator->entropyBits($passwordPreviewOptions) : null,
|
|
]);
|
|
}
|
|
}
|