Send an already signed-in operator back to the console, not the portal
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.feat/operator-identity
parent
92dd6cc2d8
commit
f970dda0bb
|
|
@ -1,5 +1,11 @@
|
||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Middleware\EnsureAdmin;
|
||||||
|
use App\Http\Middleware\EnsureCustomerActive;
|
||||||
|
use App\Http\Middleware\PublicSiteGate;
|
||||||
|
use App\Http\Middleware\RequireOperatorTwoFactor;
|
||||||
|
use App\Http\Middleware\RestrictAdminHost;
|
||||||
|
use App\Http\Middleware\RestrictConsoleNetwork;
|
||||||
use App\Support\AdminArea;
|
use App\Support\AdminArea;
|
||||||
use Illuminate\Foundation\Application;
|
use Illuminate\Foundation\Application;
|
||||||
use Illuminate\Foundation\Configuration\Exceptions;
|
use Illuminate\Foundation\Configuration\Exceptions;
|
||||||
|
|
@ -15,25 +21,25 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware): void {
|
->withMiddleware(function (Middleware $middleware): void {
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
'admin' => \App\Http\Middleware\EnsureAdmin::class,
|
'admin' => EnsureAdmin::class,
|
||||||
'admin.host' => \App\Http\Middleware\RestrictAdminHost::class,
|
'admin.host' => RestrictAdminHost::class,
|
||||||
'customer.active' => \App\Http\Middleware\EnsureCustomerActive::class,
|
'customer.active' => EnsureCustomerActive::class,
|
||||||
'operator.2fa' => \App\Http\Middleware\RequireOperatorTwoFactor::class,
|
'operator.2fa' => RequireOperatorTwoFactor::class,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Runs before the whole route stack (incl. `auth`), so /admin on a public
|
// Runs before the whole route stack (incl. `auth`), so /admin on a public
|
||||||
// hostname 404s instead of redirecting to /login and thereby revealing
|
// hostname 404s instead of redirecting to /login and thereby revealing
|
||||||
// that an operator console is hosted here. Self-scoped to /admin*.
|
// 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
|
// 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
|
// owner has listed in the console. The host guard above cannot answer
|
||||||
// that question: a Host header is chosen by the caller.
|
// 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
|
// Runs after the host guard: the console must stay reachable even while
|
||||||
// the public site is switched off.
|
// 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
|
// 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
|
// without this Laravel sees plain http and builds http:// URLs into an
|
||||||
|
|
@ -69,6 +75,19 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||||
$middleware->redirectGuestsTo(
|
$middleware->redirectGuestsTo(
|
||||||
fn (Request $request) => AdminArea::isConsole($request) ? route('admin.login') : route('login'),
|
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 {
|
->withExceptions(function (Exceptions $exceptions): void {
|
||||||
$exceptions->shouldRenderJsonWhen(
|
$exceptions->shouldRenderJsonWhen(
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,18 @@ it('logs in with valid credentials and redirects to the dashboard', function ()
|
||||||
$this->assertAuthenticatedAs($user);
|
$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 () {
|
it('rejects invalid credentials', function () {
|
||||||
$user = User::factory()->create(['password' => Hash::make('secret-password')]);
|
$user = User::factory()->create(['password' => Hash::make('secret-password')]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,21 @@ it('offers no way to register — an operator account is not self-served', funct
|
||||||
->assertDontSee(__('auth.sign_up'));
|
->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 () {
|
it('signs an operator in on the operator guard', function () {
|
||||||
$operator = Operator::factory()->role('Owner')->create(['password' => 'richtig-langes-pw']);
|
$operator = Operator::factory()->role('Owner')->create(['password' => 'richtig-langes-pw']);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue