diff --git a/app/Livewire/Admin/Settings.php b/app/Livewire/Admin/Settings.php index d1ee108..1705c20 100644 --- a/app/Livewire/Admin/Settings.php +++ b/app/Livewire/Admin/Settings.php @@ -37,6 +37,11 @@ class Settings extends Component #[Validate('required|in:Owner,Admin,Support,Billing,Read-only')] public string $staffRole = 'Support'; + /** Shown once after inviting, since email delivery is still mocked. */ + public ?string $invitedEmail = null; + + public ?string $invitedPassword = null; + public function mount(): void { $this->name = auth()->user()->name; @@ -85,14 +90,20 @@ class Settings extends Component return; } + // Email/password-setup delivery is still mocked, so generate a temporary + // password and surface it once to the Owner to share securely — the + // account is usable immediately (a proper invite link follows with mail). + $temp = Str::password(14); $user = User::create([ 'name' => $data['staffName'], 'email' => $data['staffEmail'], - 'password' => Hash::make(Str::random(40)), // invite/reset delivery is mocked + 'password' => Hash::make($temp), 'is_admin' => true, ]); $user->assignRole($data['staffRole']); + $this->invitedEmail = $data['staffEmail']; + $this->invitedPassword = $temp; $this->reset('staffName', 'staffEmail'); $this->staffRole = 'Support'; $this->dispatch('notify', message: __('admin_settings.staff_invited')); diff --git a/app/Models/MaintenanceNotification.php b/app/Models/MaintenanceNotification.php index 1ea1802..986c1e1 100644 --- a/app/Models/MaintenanceNotification.php +++ b/app/Models/MaintenanceNotification.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class MaintenanceNotification extends Model { @@ -12,4 +13,9 @@ class MaintenanceNotification extends Model { return ['sent_at' => 'datetime']; } + + public function window(): BelongsTo + { + return $this->belongsTo(MaintenanceWindow::class, 'maintenance_window_id'); + } } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 6a6c8d8..99a0ab8 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -16,6 +16,7 @@ use App\Services\Traefik\TraefikWriter; use App\Services\Wireguard\LocalWireguardHub; use App\Services\Wireguard\WireguardHub; use App\Models\MaintenanceNotification; +use Illuminate\Mail\Events\MessageSending; use Illuminate\Mail\Events\MessageSent; use Illuminate\Support\Facades\Event; use Illuminate\Support\ServiceProvider; @@ -45,6 +46,22 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { + // Suppress a queued maintenance announcement if its window was cancelled + // before the job runs — otherwise a delayed announcement could arrive for + // an already-cancelled window. Returning false aborts the send. + Event::listen(MessageSending::class, function (MessageSending $event) { + $header = $event->message->getHeaders()->get('X-CP-Notification'); + if ($header === null) { + return null; + } + $notification = MaintenanceNotification::query()->with('window')->find((int) $header->getBodyAsString()); + if ($notification?->event === 'announcement' && $notification->window?->state === 'cancelled') { + return false; // window cancelled meanwhile — do not deliver + } + + return null; + }); + // Stamp a maintenance-notification ledger row as delivered only once the // mail is actually sent (the X-CP-Notification header carries the id). // Until then sent_at stays null → the row is a retryable marker. diff --git a/lang/de/admin_settings.php b/lang/de/admin_settings.php index d93a0e0..162691e 100644 --- a/lang/de/admin_settings.php +++ b/lang/de/admin_settings.php @@ -17,6 +17,9 @@ return [ 'invite' => 'Einladen', 'invite_hint' => 'Der Zugang wird angelegt; die Einladungs-/Passwort-Zustellung folgt mit der E-Mail-Anbindung.', 'staff_invited' => 'Mitarbeiter angelegt.', + 'temp_title' => 'Zugangsdaten (einmalig sichtbar)', + 'temp_hint' => 'Teilen Sie diese Daten sicher mit dem Mitarbeiter. Beim späteren E-Mail-Setup wird ein Einladungslink versendet.', + 'temp_password' => 'Temporäres Passwort', 'col_person' => 'Person', 'col_actions' => 'Aktionen', diff --git a/lang/en/admin_settings.php b/lang/en/admin_settings.php index c788f68..0e90c10 100644 --- a/lang/en/admin_settings.php +++ b/lang/en/admin_settings.php @@ -17,6 +17,9 @@ return [ 'invite' => 'Invite', 'invite_hint' => 'The account is created; invitation/password delivery follows with the email integration.', 'staff_invited' => 'Staff member created.', + 'temp_title' => 'Credentials (shown once)', + 'temp_hint' => 'Share these securely with the staff member. A proper invitation link follows with the email integration.', + 'temp_password' => 'Temporary password', 'col_person' => 'Person', 'col_actions' => 'Actions', diff --git a/resources/views/livewire/admin/settings.blade.php b/resources/views/livewire/admin/settings.blade.php index 75fdfd1..2d95898 100644 --- a/resources/views/livewire/admin/settings.blade.php +++ b/resources/views/livewire/admin/settings.blade.php @@ -54,6 +54,17 @@

{{ __('admin_settings.invite_hint') }}

+ @if ($invitedPassword) +
+

{{ __('admin_settings.temp_title') }}

+

{{ __('admin_settings.temp_hint') }}

+
+
{{ __('admin_settings.email') }}:
{{ $invitedEmail }}
+
{{ __('admin_settings.temp_password') }}:
{{ $invitedPassword }}
+
+
+ @endif + {{-- Staff list --}}
diff --git a/tests/Feature/Admin/AdminSettingsTest.php b/tests/Feature/Admin/AdminSettingsTest.php index dec24e7..d868379 100644 --- a/tests/Feature/Admin/AdminSettingsTest.php +++ b/tests/Feature/Admin/AdminSettingsTest.php @@ -25,6 +25,17 @@ it('lets an Owner invite staff with a role', function () { expect($sam)->not->toBeNull()->and($sam->hasRole('Support'))->toBeTrue(); }); +it('surfaces a usable temporary password for the invited staff', function () { + $c = Livewire::actingAs(operator('Owner'))->test(Settings::class) + ->set('staffName', 'Ivy')->set('staffEmail', 'ivy@ops.test')->set('staffRole', 'Support') + ->call('inviteStaff'); + + $temp = $c->get('invitedPassword'); + expect($temp)->not->toBeNull(); + $ivy = User::query()->where('email', 'ivy@ops.test')->first(); + expect(Illuminate\Support\Facades\Hash::check($temp, $ivy->password))->toBeTrue(); +}); + it('forbids a non-Owner from managing staff', function () { Livewire::actingAs(operator('Admin'))->test(Settings::class) ->set('staffName', 'X')->set('staffEmail', 'x@ops.test')->set('staffRole', 'Support')