diff --git a/app/Livewire/EditMailbox.php b/app/Livewire/EditMailbox.php index 71f8557..c226c0a 100644 --- a/app/Livewire/EditMailbox.php +++ b/app/Livewire/EditMailbox.php @@ -3,6 +3,7 @@ namespace App\Livewire; use App\Models\Mailbox; +use App\Services\Secrets\SecretCipher; use LivewireUI\Modal\ModalComponent; /** @@ -57,6 +58,18 @@ class EditMailbox extends ModalComponent 'password' => ['nullable', 'string', 'max:255'], ]); + // A password can only be STORED where SECRETS_KEY exists to encrypt it + // under — the same condition the page's own banner already names. + // Checked here rather than left for Mailbox::setPasswordAttribute() to + // throw through: an uncaught RuntimeException would render Laravel's + // debug page, whose request-payload inspector can show the very + // password just typed. The page must say so, not crash on it. + if ($this->password !== '' && ! app(SecretCipher::class)->isUsable()) { + $this->addError('password', __('mail_settings.no_key')); + + return; + } + $box = Mailbox::query()->where('uuid', $this->uuid)->firstOrFail(); $box->address = $this->address; diff --git a/database/migrations/2026_07_28_110000_add_mail_manage_permission.php b/database/migrations/2026_07_28_110000_add_mail_manage_permission.php index c5c417d..6f62cc3 100644 --- a/database/migrations/2026_07_28_110000_add_mail_manage_permission.php +++ b/database/migrations/2026_07_28_110000_add_mail_manage_permission.php @@ -21,7 +21,7 @@ return new class extends Migration $permission = Permission::findOrCreate('mail.manage', 'web'); foreach (['Owner', 'Admin', 'Support'] as $role) { - Role::findByName($role, 'web')->givePermissionTo($permission); + Role::findOrCreate($role, 'web')->givePermissionTo($permission); } app(PermissionRegistrar::class)->forgetCachedPermissions(); diff --git a/tests/Feature/Mail/MailSettingsPageTest.php b/tests/Feature/Mail/MailSettingsPageTest.php index 4b41900..4bae2ee 100644 --- a/tests/Feature/Mail/MailSettingsPageTest.php +++ b/tests/Feature/Mail/MailSettingsPageTest.php @@ -97,6 +97,25 @@ it('says so when SECRETS_KEY is missing, instead of crashing on the first passwo ->assertSee(__('mail_settings.no_key')); }); +it('fails the password save with a form error, not an exception, when SECRETS_KEY is missing', function () { + // The test above only proves the PAGE says so. Nothing proved that typing + // a password into the MODAL and saving it fails gracefully — the gap a + // reviewer found: left unfixed, SecretCipher::encrypt() throws all the way + // up, and with APP_DEBUG on, Laravel's debug page can put the just-typed + // plaintext password in its request-payload inspector. All three + // password-writing tests above run under this file's valid-key beforeEach, + // so none of them would have caught it. + $box = Mailbox::factory()->create(['key' => 'support']); + + config()->set('admin_access.secrets_key', ''); + + Livewire::actingAs(User::factory()->operator('Owner')->create()) + ->test(EditMailbox::class, ['uuid' => $box->uuid]) + ->set('password', 'neu-und-sicher') + ->call('save') + ->assertHasErrors('password'); +}); + it('refuses to leave the system purpose empty, because it is the fallback', function () { Mailbox::factory()->create(['key' => 'no-reply']); Settings::set(MailPurpose::settingKey(MailPurpose::SYSTEM), 'no-reply');