From f970dda0bb5b7b070f915c017c1f19da45bd8b51 Mon Sep 17 00:00:00 2001 From: nexxo Date: Tue, 28 Jul 2026 15:55:39 +0200 Subject: [PATCH] Send an already signed-in operator back to the console, not the portal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit guest:operator's authenticated-user redirect had no console-aware override, so it fell to Laravel's own default: the named 'dashboard' route, the portal. An operator revisiting admin.login while already signed in bounced to the portal's own login instead — in exclusive mode, to a 404, since dashboard isn't a console route there. redirectUsersTo() now mirrors the existing redirectGuestsTo() override right above it, keyed on the same AdminArea::isConsole($request) condition, so the two read as a pair. --- bootstrap/app.php | 33 +++++++++++++++++++----- tests/Feature/Auth/LoginTest.php | 12 +++++++++ tests/Feature/Auth/OperatorLoginTest.php | 15 +++++++++++ 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/bootstrap/app.php b/bootstrap/app.php index 6389bc9..1bee4ce 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,11 @@ withMiddleware(function (Middleware $middleware): void { $middleware->alias([ - 'admin' => \App\Http\Middleware\EnsureAdmin::class, - 'admin.host' => \App\Http\Middleware\RestrictAdminHost::class, - 'customer.active' => \App\Http\Middleware\EnsureCustomerActive::class, - 'operator.2fa' => \App\Http\Middleware\RequireOperatorTwoFactor::class, + 'admin' => EnsureAdmin::class, + 'admin.host' => RestrictAdminHost::class, + 'customer.active' => EnsureCustomerActive::class, + 'operator.2fa' => RequireOperatorTwoFactor::class, ]); // Runs before the whole route stack (incl. `auth`), so /admin on a public // hostname 404s instead of redirecting to /login and thereby revealing // that an operator console is hosted here. Self-scoped to /admin*. - $middleware->prependToGroup('web', \App\Http\Middleware\RestrictAdminHost::class); + $middleware->prependToGroup('web', RestrictAdminHost::class); // And WHO may reach it, by network address — the VPN plus whatever the // owner has listed in the console. The host guard above cannot answer // that question: a Host header is chosen by the caller. - $middleware->prependToGroup('web', \App\Http\Middleware\RestrictConsoleNetwork::class); + $middleware->prependToGroup('web', RestrictConsoleNetwork::class); // Runs after the host guard: the console must stay reachable even while // the public site is switched off. - $middleware->appendToGroup('web', \App\Http\Middleware\PublicSiteGate::class); + $middleware->appendToGroup('web', PublicSiteGate::class); // TLS is terminated by the reverse proxy (Zoraxy on the private LAN), so // without this Laravel sees plain http and builds http:// URLs into an @@ -69,6 +75,19 @@ return Application::configure(basePath: dirname(__DIR__)) $middleware->redirectGuestsTo( fn (Request $request) => AdminArea::isConsole($request) ? route('admin.login') : route('login'), ); + + // The mirror image of redirectGuestsTo() above, same reasoning: left + // at its default, `guest:operator` sends an already signed-in + // operator revisiting admin.login through Laravel's own fallback — + // the named 'dashboard' route, the PORTAL, where the web guard is + // usually unauthenticated, bouncing them to ITS login instead. In + // exclusive mode it is worse: dashboard is not a console route at + // all, so RestrictAdminHost 404s it on the console host. Keyed on + // the same AdminArea::isConsole($request) condition as the guest + // redirect, so the two read as a pair and neither can drift from it. + $middleware->redirectUsersTo( + fn (Request $request) => AdminArea::isConsole($request) ? route('admin.overview') : route('dashboard'), + ); }) ->withExceptions(function (Exceptions $exceptions): void { $exceptions->shouldRenderJsonWhen( diff --git a/tests/Feature/Auth/LoginTest.php b/tests/Feature/Auth/LoginTest.php index e5ccffc..91dd45a 100644 --- a/tests/Feature/Auth/LoginTest.php +++ b/tests/Feature/Auth/LoginTest.php @@ -28,6 +28,18 @@ it('logs in with valid credentials and redirects to the dashboard', function () $this->assertAuthenticatedAs($user); }); +it('still sends an already signed-in customer to the portal dashboard, not the console', function () { + // The mirror of OperatorLoginTest's "sends an already signed-in operator + // back to the console" — the redirectUsersTo() override that fixes that + // is keyed on AdminArea::isConsole($request), so a plain portal request + // must stay on the path it always took. + $user = User::factory()->create(); + + $this->actingAs($user) + ->get('/login') + ->assertRedirect('/dashboard'); +}); + it('rejects invalid credentials', function () { $user = User::factory()->create(['password' => Hash::make('secret-password')]); diff --git a/tests/Feature/Auth/OperatorLoginTest.php b/tests/Feature/Auth/OperatorLoginTest.php index 5076e22..865f3a6 100644 --- a/tests/Feature/Auth/OperatorLoginTest.php +++ b/tests/Feature/Auth/OperatorLoginTest.php @@ -13,6 +13,21 @@ it('offers no way to register — an operator account is not self-served', funct ->assertDontSee(__('auth.sign_up')); }); +it('sends an already signed-in operator back to the console instead of the portal', function () { + // Codex + whole-branch review (M1), independently: guest:operator's + // authenticated-user redirect had no console-aware override, so it fell + // to Laravel's own default — the named 'dashboard' route, the PORTAL. + // The web guard is usually unauthenticated there, so an operator + // revisiting admin.login bounced straight to the portal's OWN login. In + // exclusive mode it is worse: dashboard is not a console route at all, + // so RestrictAdminHost 404s it on the console host. + $operator = Operator::factory()->role('Owner')->create(); + + $this->actingAs($operator, 'operator') + ->get(route('admin.login')) + ->assertRedirect(route('admin.overview')); +}); + it('signs an operator in on the operator guard', function () { $operator = Operator::factory()->role('Owner')->create(['password' => 'richtig-langes-pw']);