Reverb publishing is pinned to the INTERNAL reverb service (both dev * and prod compose name it `reverb`), so realtime keeps working right after a * domain change and never depends on the new public cert. The browser endpoint is * injected by the layout from DeploymentService::reverbClient(). * - app.url -> https:// so absolute URL generation matches how the panel is * served. With no domain (bare IP / dev) the env-derived app.url is left as is. */ public function boot(DeploymentService $deployment): void { // Drop Vite's hints: with only one CSS + one JS entry the preload // sits right next to the matching stylesheet/script tags, so it adds no real value — but // it triggers Chrome's "preloaded but not used" console warning (especially with DevTools // "Disable cache", which double-fetches the CSS so the preloaded copy is never consumed). Vite::usePreloadTagAttributes(false); // Re-apply the onboarding gate to EVERY Livewire component update, not just the // initial page GET. Livewire only re-runs route middleware that is registered as // persistent; without this, a component mounted while onboarded could keep running // destructive actions (hardening, SSH-key provisioning, credential changes) over // /livewire/update after the account is reverted to must_change_password mid-session. Livewire::addPersistentMiddleware([ EnsureSecurityOnboarded::class, ]); // RBAC abilities. admin has every dangerous control-plane ability; operator can run // day-to-day operations; viewer is read-only. Gates are the single source consumed by route // `can:` middleware, component mount() abort_unless, per-action guards and @can in Blade. foreach (['manage-panel', 'manage-users', 'manage-network', 'manage-fleet'] as $ability) { Gate::define($ability, fn (User $u) => $u->isAdmin()); } Gate::define('operate', fn (User $u) => $u->roleAtLeast(Role::Operator)); // Defense-in-depth: a coarse per-identity cap on the Livewire action endpoint // (/livewire/update carries every component request, including the auth actions). The // per-action limiters in the components are the precise gate; this is the blunt cap so a // single source (authed user id, else guest IP) can't flood workers regardless of which // action it targets. 180/min sits far above normal polling + interaction yet well below a // flood; it auto-expires, so — like every limiter here — it can never permanently lock the // control plane (the bare-IP recovery host and clusev:reset-admin are unaffected). RateLimiter::for('livewire-update', fn (Request $request) => Limit::perMinute(180) ->by($request->user()?->id ?: $request->ip())); Livewire::setUpdateRoute(fn ($handle) => Route::post('/livewire/update', $handle) ->middleware(['web', 'throttle:livewire-update'])); config([ 'broadcasting.connections.reverb.options.host' => 'reverb', 'broadcasting.connections.reverb.options.port' => 8080, 'broadcasting.connections.reverb.options.scheme' => 'http', 'broadcasting.connections.reverb.options.useTLS' => false, ]); $domain = $deployment->domain(); if ($domain !== null) { config(['app.url' => 'https://'.$domain]); } // Operator SMTP settings → runtime mail config. Shared helper (MailSettings::apply) because // THREE process shapes need it fresh: web requests (here, per request), the queue worker // (Queue::before, per job — boot() runs only once in that long-lived process), and the // long-lived metrics poller (AlertNotifier applies it right before queueing, since the // default MAILER NAME is baked into a mailable at queue time). MailSettings::apply(); Queue::before(fn () => MailSettings::apply()); } }