106 lines
3.7 KiB
PHP
106 lines
3.7 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
/**
|
|
* Where the operator console lives, and how to tell that a request is for it.
|
|
*
|
|
* Three guards need this answer — the hostname guard, the network allowlist and
|
|
* the public-site switch — and until now each decided for itself by testing the
|
|
* path against `admin/*`. That was survivable while the console sat under
|
|
* /admin. It stops being survivable the moment the console moves to the root of
|
|
* its own hostname: PublicSiteGate would no longer recognise it, and with the
|
|
* public site hidden the console would answer 503 to the person trying to sign
|
|
* in and turn it back on. That is the lock-out this class exists to prevent.
|
|
*
|
|
* Two modes, and only two:
|
|
*
|
|
* - A console hostname is configured. The console IS that host: it answers at
|
|
* the root, and every request to that host is a console request.
|
|
* - Nothing is configured (a fresh checkout, a development machine). The
|
|
* console stays under /admin on any host, exactly as before, so upgrading
|
|
* cannot lock anyone out of a system that was working.
|
|
*
|
|
* Read from config on every call rather than cached in a static: `config:cache`
|
|
* already freezes the value at deploy time, and a second layer of staleness in
|
|
* a long-lived worker is the kind of thing that is only discovered from the
|
|
* wrong side of a locked door.
|
|
*/
|
|
final class AdminArea
|
|
{
|
|
/** The path prefix used when no console hostname is configured. */
|
|
public const FALLBACK_PREFIX = 'admin';
|
|
|
|
/**
|
|
* The single hostname the console answers on, or null when unconfigured.
|
|
*
|
|
* ADMIN_HOSTS may list several names — an operator who reaches the console
|
|
* by IP as well as by name, for instance. The FIRST is the canonical one
|
|
* and the one URLs are generated for; the rest keep working because
|
|
* covers() accepts any of them. Dropping the alternates would remove
|
|
* exactly the recovery paths someone locked out would reach for.
|
|
*/
|
|
public static function host(): ?string
|
|
{
|
|
return self::hosts()[0] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Every hostname that reaches the console.
|
|
*
|
|
* @return array<int, string>
|
|
*/
|
|
public static function hosts(): array
|
|
{
|
|
return array_values(array_filter(array_map(
|
|
fn ($host) => strtolower(trim((string) $host)),
|
|
(array) config('admin_access.hosts', []),
|
|
)));
|
|
}
|
|
|
|
/** 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
|
|
{
|
|
return self::isHostBound()
|
|
? self::covers($request->getHost())
|
|
: $request->is(self::FALLBACK_PREFIX, 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 host-bound (the console is at the root of its own hostname),
|
|
* `admin` otherwise.
|
|
*/
|
|
public static function prefix(): string
|
|
{
|
|
return self::isHostBound() ? '' : self::FALLBACK_PREFIX;
|
|
}
|
|
|
|
/** Where the console's own front page lives, as a path. */
|
|
public static function home(): string
|
|
{
|
|
return '/'.self::prefix();
|
|
}
|
|
}
|