- 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>
217 lines
11 KiB
PHP
217 lines
11 KiB
PHP
<x-page :title="__('System Settings')" :description="__('How the site looks and what uploaders may do')">
|
||
<x-form wire:submit="saveSettings">
|
||
<x-card :title="__('Colour profile')" heading="h2" variant="outlined">
|
||
<x-stack gap="space200">
|
||
<x-scheme-picker wire:model="colorProfile" :hint="__('Choosing one previews it here. After saving, every page, mail and error page uses it.')" data-test="color-profile" />
|
||
</x-stack>
|
||
</x-card>
|
||
|
||
<x-card :title="__('Branding')" heading="h2" variant="outlined">
|
||
<x-stack gap="space200">
|
||
<x-input full wire:model="siteTitle" :label="__('Site Title')" :hint="__('Displayed as the heading on the upload, download and sign-in pages.')" />
|
||
|
||
<x-textarea full wire:model="siteDescription" :label="__('Site Description')" :hint="__('Displayed below the title on the upload, download and sign-in pages.')" rows="3" />
|
||
|
||
<x-stack gap="space200">
|
||
@if ($currentLogo)
|
||
<x-row gap="space200" wrap>
|
||
<img src="{{ Storage::disk('public')->url($currentLogo) }}" alt="{{ __('Site Logo') }}" class="admin-settings-logo" />
|
||
<x-button :label="__('Remove Logo')" icon="delete" color="error" wire:click="$set('confirmingLogoRemoval', true)" data-test="remove-logo" />
|
||
</x-row>
|
||
@endif
|
||
|
||
<x-file full wire:model="siteLogo" :label="__('Logo')" accept="image/*,.svg,.svgz" :hint="__('Max 2MB. Recommended: PNG or SVG.')" />
|
||
|
||
@if ($siteLogo && is_object($siteLogo))
|
||
@if (str_contains($siteLogo->getMimeType(), 'svg'))
|
||
<p class="md-type-body-md md-ink-variant">{{ __('SVG selected: :name', ['name' => $siteLogo->getClientOriginalName()]) }}</p>
|
||
@else
|
||
<x-stack gap="space50">
|
||
<p class="md-type-label-lg md-ink-variant">{{ __('Preview:') }}</p>
|
||
<img src="{{ $siteLogo->temporaryUrl() }}" alt="{{ __('Logo preview') }}" class="admin-settings-logo" />
|
||
</x-stack>
|
||
@endif
|
||
@endif
|
||
</x-stack>
|
||
</x-stack>
|
||
</x-card>
|
||
|
||
<x-card :title="__('Upload Protection')" heading="h2" variant="outlined">
|
||
<x-stack gap="space200">
|
||
<x-stack gap="space100">
|
||
<x-password full
|
||
wire:model="systemPassword"
|
||
:label="__('System Upload Password')"
|
||
:hint="__('Leave blank to keep current. Set a password to require it before uploading.')"
|
||
autocomplete="new-password"
|
||
/>
|
||
|
||
@if ($hasSystemPassword)
|
||
<x-button :label="__('Clear System Password')" icon="lock_reset" color="error" wire:click="$set('confirmingPasswordRemoval', true)" data-test="clear-system-password" />
|
||
@endif
|
||
</x-stack>
|
||
</x-stack>
|
||
</x-card>
|
||
|
||
{{-- How the upload page offers random share passwords (App\Services\PasswordGeneratorService).
|
||
The example is drawn from the form as it stands, before saving. --}}
|
||
<x-card :title="__('Share Passwords')" heading="h2" variant="outlined">
|
||
<x-stack gap="space200">
|
||
<x-group
|
||
wire:model.live="passwordGeneratorMode"
|
||
:label="__('Password generator')"
|
||
:hint="match ($passwordGeneratorMode) {
|
||
'off' => __('Uploaders type a password themselves.'),
|
||
'prefill' => __('A random password is filled in as soon as Password protect is switched on. Generate draws a new one.'),
|
||
default => __('A Generate button under the password field fills in a random password.'),
|
||
}"
|
||
:options="[
|
||
['id' => 'off', 'name' => __('Off')],
|
||
['id' => 'button', 'name' => __('On request')],
|
||
['id' => 'prefill', 'name' => __('Prefilled')],
|
||
]"
|
||
/>
|
||
|
||
@if ($passwordGeneratorMode !== 'off')
|
||
<x-group
|
||
wire:model.live="passwordGeneratorType"
|
||
:label="__('Kind')"
|
||
:hint="$passwordGeneratorType === 'passphrase' ? __('Random words, easy to read out or type on a phone.') : __('Random characters, the most secure for their length.')"
|
||
:options="[
|
||
['id' => 'characters', 'name' => __('Characters')],
|
||
['id' => 'passphrase', 'name' => __('Passphrase')],
|
||
]"
|
||
/>
|
||
|
||
@if ($passwordGeneratorType === 'passphrase')
|
||
<x-input full
|
||
wire:model.live.blur="passphraseWords"
|
||
:label="__('Words')"
|
||
type="number"
|
||
:min="\App\Services\PasswordGeneratorService::MIN_WORDS"
|
||
:max="\App\Services\PasswordGeneratorService::MAX_WORDS"
|
||
:hint="__('Between :min and :max.', ['min' => \App\Services\PasswordGeneratorService::MIN_WORDS, 'max' => \App\Services\PasswordGeneratorService::MAX_WORDS])"
|
||
/>
|
||
|
||
<x-select full
|
||
wire:model.live="passphraseSeparator"
|
||
:label="__('Separator')"
|
||
:options="[
|
||
['id' => 'hyphen', 'name' => __('Hyphen (-)')],
|
||
['id' => 'dot', 'name' => __('Dot (.)')],
|
||
['id' => 'underscore', 'name' => __('Underscore (_)')],
|
||
['id' => 'space', 'name' => __('Space')],
|
||
]"
|
||
/>
|
||
@else
|
||
<x-input full
|
||
wire:model.live.blur="passwordLength"
|
||
:label="__('Length')"
|
||
type="number"
|
||
:min="\App\Services\PasswordGeneratorService::MIN_LENGTH"
|
||
:max="\App\Services\PasswordGeneratorService::MAX_LENGTH"
|
||
:suffix="__('characters')"
|
||
:hint="__('Between :min and :max.', ['min' => \App\Services\PasswordGeneratorService::MIN_LENGTH, 'max' => \App\Services\PasswordGeneratorService::MAX_LENGTH])"
|
||
/>
|
||
|
||
<x-group
|
||
multiple
|
||
wire:model.live="passwordCharacterSets"
|
||
:label="__('Include')"
|
||
:hint="__('Uppercase letters, lowercase letters, numbers and symbols.')"
|
||
:options="[
|
||
['id' => 'uppercase', 'name' => 'A–Z'],
|
||
['id' => 'lowercase', 'name' => 'a–z'],
|
||
['id' => 'numbers', 'name' => '0–9'],
|
||
['id' => 'symbols', 'name' => '#$%'],
|
||
]"
|
||
/>
|
||
|
||
<x-checkbox
|
||
wire:model.live="passwordAvoidAmbiguous"
|
||
:label="__('Avoid look-alike characters')"
|
||
:hint="__('Leaves out 0, O, 1, l and I.')"
|
||
/>
|
||
@endif
|
||
|
||
@if ($passwordExample)
|
||
<x-input full
|
||
:label="__('Example')"
|
||
:value="$passwordExample"
|
||
:hint="__('About :bits bits of entropy.', ['bits' => $passwordEntropy])"
|
||
readonly
|
||
mono
|
||
data-test="password-example"
|
||
/>
|
||
@endif
|
||
@endif
|
||
</x-stack>
|
||
</x-card>
|
||
|
||
<x-card :title="__('Upload Limits')" heading="h2" variant="outlined">
|
||
<x-stack gap="space200">
|
||
<x-toggle
|
||
wire:model.live="allowNeverExpire"
|
||
:label="__('Allow shares to never expire')"
|
||
:hint="__('When disabled, users must select an expiration time.')"
|
||
right
|
||
/>
|
||
|
||
<x-select full
|
||
wire:model="defaultExpiration"
|
||
:label="__('Default Expiration')"
|
||
:placeholder="$allowNeverExpire ? __('None') : null"
|
||
:options="collect(\App\Models\Share::EXPIRATIONS)->map(fn (array $option, string $id): array => ['id' => $id, 'name' => __($option['label'])])->values()->all()"
|
||
/>
|
||
|
||
<x-input full
|
||
wire:model="maxFileSize"
|
||
:label="__('Max file size (MB)')"
|
||
type="number"
|
||
min="1"
|
||
suffix="MB"
|
||
/>
|
||
|
||
<x-input full wire:model="maxFilesPerShare" :label="__('Max files per share')" type="number" min="1" />
|
||
|
||
<x-input full wire:model="maxSizePerShare" :label="__('Max total size per share (GB)')" type="number" min="1" suffix="GB" />
|
||
</x-stack>
|
||
</x-card>
|
||
|
||
<x-card :title="__('Storage')" heading="h2" variant="outlined">
|
||
<x-stack gap="space200">
|
||
<x-input full
|
||
wire:model="maxStorageQuota"
|
||
:label="__('Max storage quota (GB)')"
|
||
type="number"
|
||
min="1"
|
||
suffix="GB"
|
||
:hint="__('When reached, new uploads are blocked.')"
|
||
/>
|
||
</x-stack>
|
||
</x-card>
|
||
|
||
<x-slot:actions>
|
||
<x-button type="submit" :label="__('Save Settings')" variant="filled" icon="check" spinner="saveSettings" data-test="save-settings" />
|
||
</x-slot:actions>
|
||
</x-form>
|
||
|
||
<x-modal wire:model="confirmingLogoRemoval" :title="__('Remove the logo?')" icon="delete">
|
||
{{ __('The upload, download and sign-in pages show only the site title.') }}
|
||
|
||
<x-slot:actions>
|
||
<x-button :label="__('Cancel')" x-on:click="close()" />
|
||
<x-button :label="__('Remove')" danger wire:click="removeLogo" data-test="confirm-remove-logo" />
|
||
</x-slot:actions>
|
||
</x-modal>
|
||
|
||
<x-modal wire:model="confirmingPasswordRemoval" :title="__('Remove the system password?')" icon="lock_open">
|
||
{{ __('Anyone who can reach the upload page can upload files again.') }}
|
||
|
||
<x-slot:actions>
|
||
<x-button :label="__('Cancel')" x-on:click="close()" />
|
||
<x-button :label="__('Remove')" danger wire:click="clearSystemPassword" data-test="confirm-clear-system-password" />
|
||
</x-slot:actions>
|
||
</x-modal>
|
||
</x-page>
|