*/ public static function hosts(): array { return array_values(array_filter(array_map( fn ($host) => strtolower(trim((string) $host)), (array) config('admin_access.hosts', []), ))); } /** * May ONLY the console answer on the console's hostname? * * Separate from being host-bound, and off by default, because the two are * not the same thing. A development machine lists its own IP and localhost * in ADMIN_HOSTS so the console is reachable without DNS — and on that * machine the portal and the public site have to keep working. Turning * exclusivity on there would 404 everything that is not the console, which * is exactly what it did the first time this shipped. * * It belongs with the routing change that moves the console to the root of * its own hostname: until then a console host legitimately serves both. */ public static function isExclusive(): bool { return self::isHostBound() && (bool) config('admin_access.exclusive', false); } /** Whether the console is bound to a hostname at all. */ public static function isHostBound(): bool { return self::hosts() !== []; } /** * Is this request addressed to the console? * * The one question the guards ask. When the console is host-bound the whole * host is the console — including its sign-in page, which is precisely what * the path test used to miss. */ public static function isConsole(Request $request): bool { // Keyed on exclusivity, not on merely having a hostname: where the // console shares its host with the portal it still lives under /admin, // and "every request to this host is the console" would be false. return self::isExclusive() ? self::covers($request->getHost()) : $request->is(self::FALLBACK_PREFIX, self::FALLBACK_PREFIX.'/*'); } /** * Does this URL lead into the console? * * Needed because the sign-in POST does not go to a console address in * shared mode — everyone posts to /login — so the request alone cannot say * whether the console is where this person was heading. The intended URL * can, and without it a non-operator turned back at /admin keeps the * session that was just created for them. */ public static function pointsAtConsole(?string $url): bool { if ($url === null || $url === '') { return false; } if (self::isExclusive()) { $host = parse_url($url, PHP_URL_HOST); return is_string($host) && self::covers($host); } $path = trim((string) parse_url($url, PHP_URL_PATH), '/'); return $path === self::FALLBACK_PREFIX || str_starts_with($path, self::FALLBACK_PREFIX.'/'); } /** Is this hostname one the console answers on? */ public static function covers(string $host): bool { return in_array(strtolower($host), self::hosts(), true); } /** * The path prefix console routes are registered under. * * Empty when the console has its hostname to itself — it is then the root * of that host. `admin` otherwise. */ public static function prefix(): string { return self::isExclusive() ? '' : self::FALLBACK_PREFIX; } /** * Is the current request on this console route, whichever hostname it came * through? * * The alternates are registered under `admin.viaN.*` so route caching can * serialise them, which means an exact name check stops matching the moment * someone reaches the console through a recovery address — and the whole * navigation quietly loses its active state exactly when things are already * going wrong. */ public static function routeIs(string $name): bool { $suffix = str_starts_with($name, 'admin.') ? substr($name, 6) : $name; return (bool) request()->routeIs('admin.'.$suffix, 'admin.via*.'.$suffix); } /** Where the console's own front page lives, as a path. */ public static function home(): string { return '/'.self::prefix(); } }