withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', channels: __DIR__.'/../routes/channels.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { $middleware->alias([ 'admin' => \App\Http\Middleware\EnsureAdmin::class, 'admin.host' => \App\Http\Middleware\RestrictAdminHost::class, 'customer.active' => \App\Http\Middleware\EnsureCustomerActive::class, 'operator.2fa' => \App\Http\Middleware\RequireOperatorTwoFactor::class, ]); // Runs before the whole route stack (incl. `auth`), so /admin on a public // hostname 404s instead of redirecting to /login and thereby revealing // that an operator console is hosted here. Self-scoped to /admin*. $middleware->prependToGroup('web', \App\Http\Middleware\RestrictAdminHost::class); // And WHO may reach it, by network address — the VPN plus whatever the // owner has listed in the console. The host guard above cannot answer // that question: a Host header is chosen by the caller. $middleware->prependToGroup('web', \App\Http\Middleware\RestrictConsoleNetwork::class); // Runs after the host guard: the console must stay reachable even while // the public site is switched off. $middleware->appendToGroup('web', \App\Http\Middleware\PublicSiteGate::class); // TLS is terminated by the reverse proxy (Zoraxy on the private LAN), so // without this Laravel sees plain http and builds http:// URLs into an // https page — every asset and redirect breaks as mixed content. // // X_FORWARDED_HOST is deliberately NOT trusted: the console is gated on // the request host (ADMIN_HOSTS), and trusting that header would let // anyone reach /admin through a public domain by sending // "X-Forwarded-Host: admin.…". Zoraxy passes the real Host through, so // nothing needs it. $middleware->trustProxies( at: ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '127.0.0.1'], headers: Request::HEADER_X_FORWARDED_FOR | Request::HEADER_X_FORWARDED_PORT | Request::HEADER_X_FORWARDED_PROTO, ); // Stripe posts server-to-server with its own signature (no CSRF token). $middleware->validateCsrfTokens(except: ['webhooks/stripe']); // ApplicationBuilder::withMiddleware() defaults this to // fn () => route('login') BEFORE this callback ever runs — always the // PORTAL's Fortify page, regardless of which side of the site the // guest was actually turned away from. In non-exclusive (shared) // mode that sends a guest bounced off /admin to a sign-in form whose // table holds no operators; exclusive mode happened to self-correct // only because RestrictAdminHost 404s the wrong host before this // ever ran. Fixed for both modes at once, rather than relying on // which one a given deployment happens to run: ask AdminArea, which // both routes/web.php's own registration and RestrictAdminHost // already treat as the one source of truth for "is this the // console". $middleware->redirectGuestsTo( fn (Request $request) => AdminArea::isConsole($request) ? route('admin.login') : route('login'), ); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( fn (Request $request) => $request->is('api/*'), ); })->create();