99 lines
5.0 KiB
PHP
99 lines
5.0 KiB
PHP
<?php
|
|
|
|
namespace App\Providers;
|
|
|
|
use App\Enums\Role;
|
|
use App\Http\Middleware\EnsureSecurityOnboarded;
|
|
use App\Models\User;
|
|
use App\Services\DeploymentService;
|
|
use App\Support\MailSettings;
|
|
use Illuminate\Cache\RateLimiting\Limit;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\RateLimiter;
|
|
use Illuminate\Support\Facades\Route;
|
|
use Illuminate\Support\Facades\Vite;
|
|
use Illuminate\Support\ServiceProvider;
|
|
use Livewire\Livewire;
|
|
|
|
class AppServiceProvider extends ServiceProvider
|
|
{
|
|
public function register(): void
|
|
{
|
|
//
|
|
}
|
|
|
|
/**
|
|
* Apply the effective panel domain (dashboard override in DB, else install-time
|
|
* APP_DOMAIN) to runtime config at boot. boot() runs every request, so a domain
|
|
* change takes effect on the next request after the operator restarts the stack —
|
|
* nothing is switched mid-session, which is what makes it safe.
|
|
*
|
|
* - Server -> 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://<domain> 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 <link rel="preload"> 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());
|
|
}
|
|
}
|