98 lines
4.2 KiB
PHP
98 lines
4.2 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Support\AdminArea;
|
|
use App\Support\Settings;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Symfony\Component\HttpFoundation\IpUtils;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
|
|
/**
|
|
* Hides the marketing site and the customer portal while the product is still
|
|
* being built, without hiding it from us.
|
|
*
|
|
* Switched from the console (site.public). Anyone coming through the management
|
|
* VPN, and any signed-in operator, sees the real thing; everyone else — crawlers
|
|
* included — gets a placeholder.
|
|
*
|
|
* Answers 503 with Retry-After and X-Robots-Tag rather than 200: a 200 would
|
|
* invite search engines to index the placeholder as the site's content, and
|
|
* getting that out of an index again is much harder than keeping it out.
|
|
*/
|
|
class PublicSiteGate
|
|
{
|
|
/**
|
|
* Paths that must keep working regardless: the console itself (otherwise the
|
|
* switch could not be flipped back), Stripe's webhook, and the health check.
|
|
*
|
|
* Livewire is deliberately NOT in this list. Its endpoint is shared by the
|
|
* console and the portal, so exempting it would leave a signed-in customer
|
|
* able to drive portal components — including billing — while the portal is
|
|
* supposed to be offline. Operators pass the check below anyway, which is
|
|
* what the console actually needs.
|
|
*
|
|
* Nor is /login, and that has a consequence worth knowing before you hide
|
|
* the site: /admin sends a guest to /login, which is not the console, so
|
|
* SIGNING IN while hidden needs the request to come from a trusted range —
|
|
* the management VPN, or an address put in TRUSTED_RANGES for the initial
|
|
* setup. Exempting the login flow by hostname instead would mean trusting a
|
|
* Host header, which the caller chooses, and one forged header would unhide
|
|
* the entire portal.
|
|
*/
|
|
private const ALWAYS_ALLOWED = ['webhooks/*', 'up', 'robots.txt'];
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
// The console is exempt wherever it currently lives. This used to be
|
|
// the literal paths 'admin' and 'admin/*', which stops matching the
|
|
// moment the console moves to the root of its own hostname — and the
|
|
// result would be 503 on the sign-in page of the console, which is the
|
|
// only place the switch can be turned back on. Asking AdminArea keeps
|
|
// the exemption attached to the console rather than to a path that
|
|
// happened to be true once.
|
|
if (AdminArea::isConsole($request) || $request->is(...self::ALWAYS_ALLOWED) || Settings::bool('site.public', true)) {
|
|
return $next($request);
|
|
}
|
|
|
|
if ($this->fromManagementNetwork($request) || $this->isActiveOperator()) {
|
|
return $next($request);
|
|
}
|
|
|
|
return response()->view('coming-soon', [], 503)->withHeaders([
|
|
'Retry-After' => 3600,
|
|
'X-Robots-Tag' => 'noindex, nofollow, noarchive',
|
|
'Cache-Control' => 'no-store',
|
|
]);
|
|
}
|
|
|
|
private function fromManagementNetwork(Request $request): bool
|
|
{
|
|
$ranges = (array) config('admin_access.trusted_ranges', []);
|
|
|
|
return $ranges !== [] && IpUtils::checkIp((string) $request->ip(), $ranges);
|
|
}
|
|
|
|
/**
|
|
* Same standard as EnsureAdmin: an active operator holding a console role.
|
|
* A bare `check()` stays true for a disabled operator's live session —
|
|
* disabling someone did not sign them out, so it never stopped this gate
|
|
* from waving them through indefinitely.
|
|
*
|
|
* Deliberately checked, not enforced by logging out: this gate only
|
|
* decides what ONE request sees, and destroying the session would ripple
|
|
* into whatever else that session is doing (a concurrent console tab,
|
|
* mid-task) — a much bigger side effect than a visibility check exists to
|
|
* have. EnsureAdmin sets the precedent: it also 403s a disabled operator
|
|
* per request without touching their session.
|
|
*/
|
|
private function isActiveOperator(): bool
|
|
{
|
|
$operator = Auth::guard('operator')->user();
|
|
|
|
return $operator !== null && $operator->isActive() && $operator->can('console.view');
|
|
}
|
|
}
|