diff --git a/bootstrap/app.php b/bootstrap/app.php index 1f3cff6..6389bc9 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,6 @@ 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'), + ); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/tests/Feature/Admin/AdminConsoleTest.php b/tests/Feature/Admin/AdminConsoleTest.php index 7ab5870..5862140 100644 --- a/tests/Feature/Admin/AdminConsoleTest.php +++ b/tests/Feature/Admin/AdminConsoleTest.php @@ -5,10 +5,44 @@ use App\Models\User; $adminRoutes = ['admin.overview', 'admin.customers', 'admin.instances', 'admin.hosts', 'admin.provisioning', 'admin.revenue']; -it('redirects guests from the admin console to login', function (string $route) { - $this->get(route($route))->assertRedirect('/login'); +it('redirects guests from the admin console to the CONSOLE login, not the portal one', function (string $route) { + // Not '/login': that is Fortify's portal page, whose table holds no + // operators — a sign-in a guest bounced off the console could never + // complete. bootstrap/app.php's redirectGuestsTo() asks AdminArea which + // side this request was for. This suite's default (ADMIN_HOSTS="", + // phpunit.xml) is non-exclusive/shared mode; the exclusive-mode side of + // the same fix is covered separately below. + $this->get(route($route))->assertRedirect(route('admin.login')); })->with($adminRoutes); +it('sends a guest to the right login for whichever side they hit, in exclusive mode too', function () { + // The other of the two modes redirectGuestsTo() has to get right — see + // the shared-mode version of this test above. AdminArea::isConsole() + // reads admin_access.* LIVE, per request (unlike route domain-binding, + // which this suite's routes do not carry: they are compiled once at + // boot under the default non-exclusive ADMIN_HOSTS="", so both + // route('admin.login') and route('login') below resolve against + // whichever host the current request came in on, exactly as production + // would resolve them for a request truly received on that host). + config()->set('admin_access.hosts', ['admin.example.test']); + config()->set('admin_access.exclusive', true); + + $consoleLocation = $this->get('http://admin.example.test/admin') + ->assertRedirect()->headers->get('Location'); + + expect(parse_url($consoleLocation, PHP_URL_HOST))->toBe('admin.example.test') + ->and(parse_url($consoleLocation, PHP_URL_PATH))->toBe('/admin/login'); + + // A different, non-console host in the same exclusive setup still gets + // the PORTAL's sign-in page — this fix is not a blanket change to every + // guest redirect, only to the ones landing on the console side. + $portalLocation = $this->get('http://app.example.test/dashboard') + ->assertRedirect()->headers->get('Location'); + + expect(parse_url($portalLocation, PHP_URL_HOST))->toBe('app.example.test') + ->and(parse_url($portalLocation, PHP_URL_PATH))->toBe('/login'); +}); + it('forbids non-admin users from the admin console', function (string $route) { $user = User::factory()->create(); diff --git a/tests/Feature/Admin/DatacenterTest.php b/tests/Feature/Admin/DatacenterTest.php index a6400c9..934ad7b 100644 --- a/tests/Feature/Admin/DatacenterTest.php +++ b/tests/Feature/Admin/DatacenterTest.php @@ -11,7 +11,7 @@ use Livewire\Livewire; // admin() helper is defined in HostManagementTest (global Pest function). it('gates the datacenters page', function () { - $this->get(route('admin.datacenters'))->assertRedirect('/login'); + $this->get(route('admin.datacenters'))->assertRedirect(route('admin.login')); $this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.datacenters'))->assertForbidden(); $this->actingAs(admin(), 'operator')->get(route('admin.datacenters'))->assertOk()->assertSee(__('datacenters.title')); }); diff --git a/tests/Feature/Admin/HostManagementTest.php b/tests/Feature/Admin/HostManagementTest.php index d94ebaa..1d29ffd 100644 --- a/tests/Feature/Admin/HostManagementTest.php +++ b/tests/Feature/Admin/HostManagementTest.php @@ -15,14 +15,14 @@ use Illuminate\Support\Facades\Queue; use Livewire\Livewire; it('gates the add-host page', function () { - $this->get(route('admin.hosts.create'))->assertRedirect('/login'); + $this->get(route('admin.hosts.create'))->assertRedirect(route('admin.login')); $this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.hosts.create'))->assertForbidden(); $this->actingAs(admin(), 'operator')->get(route('admin.hosts.create'))->assertOk()->assertSee(__('hosts.create_title')); }); it('gates the host detail page', function () { $host = Host::factory()->create(); - $this->get(route('admin.hosts.show', $host))->assertRedirect('/login'); + $this->get(route('admin.hosts.show', $host))->assertRedirect(route('admin.login')); $this->actingAs(User::factory()->create(['is_admin' => false]))->get(route('admin.hosts.show', $host))->assertForbidden(); $this->actingAs(admin(), 'operator')->get(route('admin.hosts.show', $host))->assertOk()->assertSee($host->name); }); diff --git a/tests/Feature/Provisioning/LiveProgressTest.php b/tests/Feature/Provisioning/LiveProgressTest.php index 537881d..2c53ba4 100644 --- a/tests/Feature/Provisioning/LiveProgressTest.php +++ b/tests/Feature/Provisioning/LiveProgressTest.php @@ -22,7 +22,7 @@ it('shows real provisioning runs to admins', function () { }); it('gates the provisioning console to admins', function () { - $this->get(route('admin.provisioning'))->assertRedirect('/login'); + $this->get(route('admin.provisioning'))->assertRedirect(route('admin.login')); $this->actingAs(User::factory()->create()) ->get(route('admin.provisioning'))->assertForbidden(); });