69 lines
2.7 KiB
PHP
69 lines
2.7 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Middleware;
|
|
|
|
use App\Support\Settings;
|
|
use Closure;
|
|
use Illuminate\Http\Request;
|
|
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 = ['admin', 'admin/*', 'webhooks/*', 'up', 'robots.txt'];
|
|
|
|
public function handle(Request $request, Closure $next): Response
|
|
{
|
|
if ($request->is(...self::ALWAYS_ALLOWED) || Settings::bool('site.public', true)) {
|
|
return $next($request);
|
|
}
|
|
|
|
if ($this->fromManagementNetwork($request) || $request->user()?->isOperator()) {
|
|
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);
|
|
}
|
|
}
|