withRouting( web: __DIR__.'/../routes/web.php', commands: __DIR__.'/../routes/console.php', channels: __DIR__.'/../routes/channels.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { // The one endpoint that must answer WHILE the application is down. // // It is the deployment's own status, read by the overlay every three // seconds. Behind maintenance mode it answered 503 for the whole run, // so the overlay had nothing to show but "still waiting" and could not // name the step it was on — and any Livewire request that fired in the // same window swapped the console for Laravel's 503 page. // // Safe to exempt: it reports version, commit and a step name, it is // behind the console's host and network guards, and it writes nothing. $middleware->preventRequestsDuringMaintenance(except: [ 'update/state', '*/update/state', ]); $middleware->alias([ 'admin' => EnsureAdmin::class, 'admin.host' => RestrictAdminHost::class, 'customer.active' => EnsureCustomerActive::class, 'operator.2fa' => 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', 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', RestrictConsoleNetwork::class); // Runs after the host guard: the console must stay reachable even while // the public site is switched off. $middleware->appendToGroup('web', PublicSiteGate::class); // Keeps each login_sessions row pointing at the session it describes. // Laravel rotates the session id on sign-in and on every regenerate(), // so a row written once stops matching almost immediately — and the // list would hide the session its reader is sitting in. $middleware->appendToGroup('web', TouchLoginSession::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'), ); // The mirror image of redirectGuestsTo() above, same reasoning: left // at its default, `guest:operator` sends an already signed-in // operator revisiting admin.login through Laravel's own fallback — // the named 'dashboard' route, the PORTAL, where the web guard is // usually unauthenticated, bouncing them to ITS login instead. In // exclusive mode it is worse: dashboard is not a console route at // all, so RestrictAdminHost 404s it on the console host. Keyed on // the same AdminArea::isConsole($request) condition as the guest // redirect, so the two read as a pair and neither can drift from it. $middleware->redirectUsersTo( fn (Request $request) => AdminArea::isConsole($request) ? route('admin.overview') : route('dashboard'), ); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( fn (Request $request) => $request->is('api/*'), ); })->create();