diff --git a/src/Http/Middleware/EnsureTwoFactorEnabled.php b/src/Http/Middleware/EnsureTwoFactorEnabled.php index a375bd5..ce47df6 100644 --- a/src/Http/Middleware/EnsureTwoFactorEnabled.php +++ b/src/Http/Middleware/EnsureTwoFactorEnabled.php @@ -87,13 +87,17 @@ protected function shouldPassThrough(Request $request): bool return true; } + // trim() empties a root Nova path ("/"), so build the prefix with a + // trailing slash only when there is one — otherwise the patterns would + // gain a leading slash and never match Request::is()'s slash-less path. $novaPath = trim(Nova::path(), '/'); + $prefix = $novaPath === '' ? '' : "{$novaPath}/"; // Anti-lockout: never block Nova's own assets, logout, or the enrolment // page, or an admin could brick themselves / break the SPA. Not // configurable — a consumer editing except.* can never strip these. if ($request->routeIs('nova.asset.*', 'nova.logout') - || $request->is("{$novaPath}/user-security", "{$novaPath}/user-security/*") + || $request->is("{$prefix}user-security", "{$prefix}user-security/*") ) { return true; } diff --git a/tests/Feature/MiddlewareTest.php b/tests/Feature/MiddlewareTest.php index 1ca767e..8ece625 100644 --- a/tests/Feature/MiddlewareTest.php +++ b/tests/Feature/MiddlewareTest.php @@ -133,6 +133,35 @@ function loginAdmin(bool $enrolled = false): User get('/nova/user-security/confirm')->assertOk()->assertSee('confirm'); }); +it('allow-lists the user-security subtree when the Nova path is root ("/")', function (): void { + // A root-mounted Nova (config nova.path = "/") makes Nova::path() return "/"; + // the enrolment subtree must still be reachable so an un-enrolled admin can set 2FA up. + config(['nova.path' => '/']); + + Route::middleware(['web', EnsureTwoFactorEnabled::class])->group(function (): void { + Route::get('/user-security', fn (): string => 'security')->name('nova.pages.user-security'); + Route::get('/user-security/two-factor-authentication', fn (): string => 'enable'); + }); + + loginAdmin(); // un-enrolled + + get('/user-security', ['Accept' => 'text/html'])->assertOk()->assertSee('security'); + get('/user-security/two-factor-authentication', ['Accept' => 'text/html'])->assertOk()->assertSee('enable'); +}); + +it('still redirects an un-enrolled admin to user security when the Nova path is root ("/")', function (): void { + config(['nova.path' => '/']); + + Route::middleware(['web', EnsureTwoFactorEnabled::class])->group(function (): void { + Route::get('/dashboard', fn (): string => 'dash'); + Route::get('/user-security', fn (): string => 'security')->name('nova.pages.user-security'); + }); + + loginAdmin(); + + get('/dashboard', ['Accept' => 'text/html'])->assertRedirect(route('nova.pages.user-security')); +}); + it('never blocks the Fortify 2FA enrolment endpoints under user-security', function (string $path): void { Route::middleware(['web', EnsureTwoFactorEnabled::class])->get($path, fn (): string => 'ok'); loginAdmin(); // un-enrolled — must still reach every enrolment endpoint