From bcd734c03096db26384022d728d97193fa6f7356 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Tue, 7 Jul 2026 14:32:46 -0500 Subject: [PATCH 01/10] Register POST login route --- routes/web.php | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/web.php b/routes/web.php index e89e7eb6d26..7afa80c04e7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -19,6 +19,7 @@ if (Edition::get()->registersFrontendUserRoutes()) { if (Cms::config()->loginPath !== false) { Route::get(Cms::config()->loginPath, [LoginController::class, 'showLogin']); + Route::post(Cms::config()->loginPath, [LoginController::class, 'attemptLogin']); Route::get(CpAuthPath::TwoFactorChallenge->value, [TwoFactorAuthenticationController::class, 'showForm']); } From f239af28c1c2b3142609d1c5d351ded56d36804c Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Tue, 7 Jul 2026 15:07:27 -0500 Subject: [PATCH 02/10] Unrelated route deletion bug You were previously unable to delete the "home" route becuase `uriParts` was not on the `$route` variable --- src/Route/Routes.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Route/Routes.php b/src/Route/Routes.php index c5d7c75332e..09b14939400 100644 --- a/src/Route/Routes.php +++ b/src/Route/Routes.php @@ -180,7 +180,7 @@ public function deleteRouteByUid(string $routeUid): bool } event(new RouteDeleting(new Route( - uriParts: $route['uriParts'], + uriParts: $route['uriParts'] ?? [], template: $route['template'], siteUid: $route['siteUid'], uid: $routeUid, @@ -192,7 +192,7 @@ public function deleteRouteByUid(string $routeUid): bool ); event(new RouteDeleted(new Route( - uriParts: $route['uriParts'], + uriParts: $route['uriParts'] ?? [], template: $route['template'], siteUid: $route['siteUid'], uid: $routeUid, From adb03a4e6c0fa73f38379abb77304dab23e4b930 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Tue, 7 Jul 2026 15:07:59 -0500 Subject: [PATCH 03/10] Align post-logout with Craft 5 --- src/Http/Controllers/Auth/LoginController.php | 4 +--- .../Controllers/Auth/LoginControllerTest.php | 22 +++++++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Http/Controllers/Auth/LoginController.php b/src/Http/Controllers/Auth/LoginController.php index 14a00b028f5..1c8eb4d3c67 100644 --- a/src/Http/Controllers/Auth/LoginController.php +++ b/src/Http/Controllers/Auth/LoginController.php @@ -180,8 +180,6 @@ public function logout(Request $request): Response return redirect(cp_url(CpAuthPath::Login->value)); } - return $this->asSuccess( - redirect: $this->generalConfig->getPostLogoutRedirect(), - ); + return redirect(\CraftCms\Cms\Support\Url::siteUrl($this->generalConfig->getPostLogoutRedirect())); } } diff --git a/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php b/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php index f786fde5793..a5da3fae093 100644 --- a/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php +++ b/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php @@ -145,6 +145,28 @@ expect(Auth::check())->toBeFalse(); }); +test('logout redirects to the post-logout redirect, not back to the previous page', function () { + Cms::config()->postLogoutRedirect = ''; + + actingAs(User::findOne()); + + // Even when arriving from a page, logout must not fall through to back(). + $response = $this->from('https://localhost/members/dashboard') + ->get('/'.Cms::config()->getLogoutPath()) + ->assertRedirect(); + + expect($response->headers->get('Location'))->toBe('https://localhost/'); +}); + +test('logout honors a configured post-logout redirect', function () { + Cms::config()->postLogoutRedirect = 'goodbye'; + + actingAs(User::findOne()); + + $this->get('/'.Cms::config()->getLogoutPath()) + ->assertRedirect('https://localhost/goodbye'); +}); + test('showLoginModal requires email parameter', function () { postJson(action([LoginController::class, 'showLoginModal']), []) ->assertJsonValidationErrors(['email']); From 900d062a35b316adf7633ba8e788f86a92737073 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Tue, 7 Jul 2026 15:16:21 -0500 Subject: [PATCH 04/10] Better, more global, shorter --- src/Http/Controllers/Auth/LoginController.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Http/Controllers/Auth/LoginController.php b/src/Http/Controllers/Auth/LoginController.php index 1c8eb4d3c67..06b7f580e57 100644 --- a/src/Http/Controllers/Auth/LoginController.php +++ b/src/Http/Controllers/Auth/LoginController.php @@ -31,6 +31,7 @@ use Tpetry\QueryExpressions\Function\String\Lower; use function CraftCms\Cms\cp_url; +use function CraftCms\Cms\site_url; use function CraftCms\Cms\template; readonly class LoginController extends AuthenticationController @@ -180,6 +181,6 @@ public function logout(Request $request): Response return redirect(cp_url(CpAuthPath::Login->value)); } - return redirect(\CraftCms\Cms\Support\Url::siteUrl($this->generalConfig->getPostLogoutRedirect())); + return redirect(site_url($this->generalConfig->getPostLogoutRedirect())); } } From 43a82bb162ac523dfdc8e7491c2d78f24c3b9a35 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Tue, 7 Jul 2026 15:21:12 -0500 Subject: [PATCH 05/10] Handle localized values --- routes/web.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/routes/web.php b/routes/web.php index 7afa80c04e7..3debf525c73 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,9 +17,10 @@ $routes = app(CraftRoutes::class); if (Edition::get()->registersFrontendUserRoutes()) { - if (Cms::config()->loginPath !== false) { - Route::get(Cms::config()->loginPath, [LoginController::class, 'showLogin']); - Route::post(Cms::config()->loginPath, [LoginController::class, 'attemptLogin']); + $loginPath = Cms::config()->getLoginPath(); + if ($loginPath !== false) { + Route::get($loginPath, [LoginController::class, 'showLogin']); + Route::post($loginPath, [LoginController::class, 'attemptLogin']); Route::get(CpAuthPath::TwoFactorChallenge->value, [TwoFactorAuthenticationController::class, 'showForm']); } From 6f38c27ab0ae65e232c1b360dedca5eb2d5a78a4 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Tue, 7 Jul 2026 15:24:50 -0500 Subject: [PATCH 06/10] Revert "Handle localized values" This reverts commit 43a82bb162ac523dfdc8e7491c2d78f24c3b9a35. --- routes/web.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/routes/web.php b/routes/web.php index 3debf525c73..7afa80c04e7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,10 +17,9 @@ $routes = app(CraftRoutes::class); if (Edition::get()->registersFrontendUserRoutes()) { - $loginPath = Cms::config()->getLoginPath(); - if ($loginPath !== false) { - Route::get($loginPath, [LoginController::class, 'showLogin']); - Route::post($loginPath, [LoginController::class, 'attemptLogin']); + if (Cms::config()->loginPath !== false) { + Route::get(Cms::config()->loginPath, [LoginController::class, 'showLogin']); + Route::post(Cms::config()->loginPath, [LoginController::class, 'attemptLogin']); Route::get(CpAuthPath::TwoFactorChallenge->value, [TwoFactorAuthenticationController::class, 'showForm']); } From 3d342e4899866a34f800ffd5664e27a64fbe3abe Mon Sep 17 00:00:00 2001 From: Rias Date: Tue, 7 Jul 2026 23:06:33 +0200 Subject: [PATCH 07/10] Fix localized site routes --- routes/web.php | 36 +++++++++++-------- src/Auth/AuthServiceProvider.php | 2 +- src/Providers/AppServiceProvider.php | 6 ++-- src/Route/Routes.php | 16 +++++++++ src/View/TemplateGlobals.php | 8 ++--- .../Controllers/Auth/LoginControllerTest.php | 17 +++++++++ 6 files changed, 64 insertions(+), 21 deletions(-) diff --git a/routes/web.php b/routes/web.php index 7afa80c04e7..f78337d5948 100644 --- a/routes/web.php +++ b/routes/web.php @@ -18,26 +18,34 @@ if (Edition::get()->registersFrontendUserRoutes()) { if (Cms::config()->loginPath !== false) { - Route::get(Cms::config()->loginPath, [LoginController::class, 'showLogin']); - Route::post(Cms::config()->loginPath, [LoginController::class, 'attemptLogin']); Route::get(CpAuthPath::TwoFactorChallenge->value, [TwoFactorAuthenticationController::class, 'showForm']); } - if (Cms::config()->verifyEmailPath !== false) { - Route::get(Cms::config()->verifyEmailPath, [VerifyEmailController::class, 'show']); - Route::post(Cms::config()->verifyEmailPath, [VerifyEmailController::class, 'store']); - } + /* + * These paths can be localized per site, so a route is registered for every + * site's value. Changing them or the sites requires re-running `route:cache` + * on installs that cache routes. + */ + if (Cms::isInstalled()) { + foreach ($routes->localizedConfigPaths('getLoginPath') as $path) { + Route::get($path, [LoginController::class, 'showLogin']); + Route::post($path, [LoginController::class, 'attemptLogin']); + } - if (Cms::config()->setPasswordPath !== false) { - Route::get(Cms::config()->setPasswordPath, [SetPasswordController::class, 'show']); - Route::post(Cms::config()->setPasswordPath, [SetPasswordController::class, 'store']); - } + foreach ($routes->localizedConfigPaths('getVerifyEmailPath') as $path) { + Route::get($path, [VerifyEmailController::class, 'show']); + Route::post($path, [VerifyEmailController::class, 'store']); + } - Route::middleware('auth')->group(function () { - if (Cms::config()->logoutPath !== false) { - Route::get(Cms::config()->logoutPath, [LoginController::class, 'logout']); + foreach ($routes->localizedConfigPaths('getSetPasswordPath') as $path) { + Route::get($path, [SetPasswordController::class, 'show']); + Route::post($path, [SetPasswordController::class, 'store']); } - }); + + foreach ($routes->localizedConfigPaths('getLogoutPath') as $path) { + Route::get($path, [LoginController::class, 'logout'])->middleware('auth'); + } + } } if (OAuth::isAvailable()) { diff --git a/src/Auth/AuthServiceProvider.php b/src/Auth/AuthServiceProvider.php index 2e7677fa6b5..99e2ccc02ba 100644 --- a/src/Auth/AuthServiceProvider.php +++ b/src/Auth/AuthServiceProvider.php @@ -69,7 +69,7 @@ private function bootRedirects(): void return Cms::config()->cpTrigger.'/login'; } - return Cms::config()->loginPath; + return Cms::config()->getLoginPath(); }); RedirectIfAuthenticated::redirectUsing(fn (Request $request) => URL::defaultReturnUrl()); diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index d0cecafd3b8..92bb60adebd 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -72,8 +72,10 @@ public function register(): void public function boot(): void { AuthenticationException::redirectUsing(function () { - if (! request()->isCpRequest() && Cms::config()->loginPath !== false) { - return Url::siteUrl(Cms::config()->getLoginPath()); + $loginPath = Cms::config()->getLoginPath(); + + if (! request()->isCpRequest() && $loginPath !== false) { + return Url::siteUrl($loginPath); } return Url::cpUrl(CpAuthPath::Login->value); diff --git a/src/Route/Routes.php b/src/Route/Routes.php index 09b14939400..2110648ce1d 100644 --- a/src/Route/Routes.php +++ b/src/Route/Routes.php @@ -11,6 +11,7 @@ use CraftCms\Cms\Route\Events\RouteDeleting; use CraftCms\Cms\Route\Events\RouteSaved; use CraftCms\Cms\Route\Events\RouteSaving; +use CraftCms\Cms\Site\Data\Site; use CraftCms\Cms\Site\Events\SiteDeleted; use CraftCms\Cms\Site\Exceptions\SiteNotFoundException; use CraftCms\Cms\Site\Sites; @@ -87,6 +88,21 @@ public function __construct( private readonly Sites $sites, ) {} + /** + * Returns the unique localized values of a GeneralConfig path setting across all sites, + * for settings that can be defined per site (a site handle keyed array or callable). + * + * @return Collection + */ + public function localizedConfigPaths(string $getter): Collection + { + return $this->sites->getAllSites() + ->map(fn (Site $site) => Cms::config()->$getter($site->handle)) + ->filter(fn (mixed $path) => is_string($path) && $path !== '') + ->unique() + ->values(); + } + /** * Returns the routes defined in the project config. * diff --git a/src/View/TemplateGlobals.php b/src/View/TemplateGlobals.php index 64f3a57ce3c..2da798642cc 100644 --- a/src/View/TemplateGlobals.php +++ b/src/View/TemplateGlobals.php @@ -57,11 +57,11 @@ public function resolve(): array 'language' => app()->getLocale(), 'devMode' => $this->app->hasDebugModeEnabled(), 'isInstalled' => $isInstalled, - 'loginUrl' => $this->generalConfig->loginPath !== false - ? Url::siteUrl($this->generalConfig->getLoginPath()) + 'loginUrl' => is_string($loginPath = $this->generalConfig->getLoginPath()) + ? Url::siteUrl($loginPath) : null, - 'logoutUrl' => $this->generalConfig->logoutPath !== false - ? Url::siteUrl($this->generalConfig->getLogoutPath()) + 'logoutUrl' => is_string($logoutPath = $this->generalConfig->getLogoutPath()) + ? Url::siteUrl($logoutPath) : null, 'setPasswordUrl' => $setPasswordRequestPath !== null ? Url::siteUrl($setPasswordRequestPath) : null, 'now' => now(), diff --git a/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php b/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php index a5da3fae093..86fcd4347e6 100644 --- a/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php +++ b/tests/Feature/Http/Controllers/Auth/LoginControllerTest.php @@ -17,6 +17,7 @@ use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Event; +use Illuminate\Support\Facades\Route; use Inertia\Testing\AssertableInertia; use function CraftCms\Cms\cp_url; @@ -248,6 +249,22 @@ expect(Auth::check())->toBeTrue(); }); +test('login routes are registered for localized loginPath values', function () { + Cms::config()->isSystemLive = true; + Cms::config()->loginPath = ['siteWithCustomPath' => 'aanmelden']; + + Route::middleware(['web', 'craft', 'craft.web'])->group(dirname(__DIR__, 5).'/routes/web.php'); + + $user = User::findOne(); + + postJson('/aanmelden', [ + 'loginName' => $user->email, + 'password' => 'craftcms2018!!', + ])->assertOk(); + + expect(Auth::check())->toBeTrue(); +}); + test('attemptLogin respects LoginUserRetrieving event', function () { $user = UserModel::factory()->admin()->create([ 'email' => 'event-user@example.com', From 670c97b574acddf4da55e21b07d007f37a0e98cb Mon Sep 17 00:00:00 2001 From: Rias Date: Tue, 7 Jul 2026 23:12:02 +0200 Subject: [PATCH 08/10] phpstan + changelog --- CHANGELOG.md | 4 ++++ src/Route/Routes.php | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d9a9c8abc..e021052575b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Release Notes for Craft CMS 6 +## Unreleased + +- Fixed a bug where site routes weren't being registered for each localized site value. + ## 6.0.0-alpha.11 - 2026-07-07 - Users can now connect their accounts to one or more Socialite providers. ([#19202](https://github.com/craftcms/cms/pull/19202)) diff --git a/src/Route/Routes.php b/src/Route/Routes.php index 2110648ce1d..8aec3531faf 100644 --- a/src/Route/Routes.php +++ b/src/Route/Routes.php @@ -92,7 +92,7 @@ public function __construct( * Returns the unique localized values of a GeneralConfig path setting across all sites, * for settings that can be defined per site (a site handle keyed array or callable). * - * @return Collection + * @return Collection */ public function localizedConfigPaths(string $getter): Collection { From bf77a1a7d7f27a37a768fb092e9a0c54a05e3ec1 Mon Sep 17 00:00:00 2001 From: Brian Hanson Date: Wed, 8 Jul 2026 07:09:55 -0500 Subject: [PATCH 09/10] Make login accessible via `actions/users/login` --- routes/actions.php | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/actions.php b/routes/actions.php index 0b9e8f6eccd..265e57e86fe 100644 --- a/routes/actions.php +++ b/routes/actions.php @@ -127,6 +127,7 @@ Route::post('auth/verify-recovery-code', [TwoFactorAuthenticationController::class, 'verifyRecoveryCode']); }); Route::post('auth/passkey-request-options', [PasskeyController::class, 'requestOptions']); + Route::post('users/login', [LoginController::class, 'attemptLogin']); Route::post('users/login-with-passkey', [PasskeyController::class, 'login']); Route::post('users/login-modal', [LoginController::class, 'showLoginModal']); Route::any('users/redirect', [LoginController::class, 'redirect']); From d3b4154178fcf98756ce98cc6395feba510e8328 Mon Sep 17 00:00:00 2001 From: brandonkelly Date: Wed, 8 Jul 2026 11:31:14 -0700 Subject: [PATCH 10/10] Always call GeneralConfig::getLoginPath() --- routes/web.php | 2 +- .../Feature/Http/Controllers/Auth/OAuthControllerTest.php | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/routes/web.php b/routes/web.php index f78337d5948..d710d1e4c8f 100644 --- a/routes/web.php +++ b/routes/web.php @@ -17,7 +17,7 @@ $routes = app(CraftRoutes::class); if (Edition::get()->registersFrontendUserRoutes()) { - if (Cms::config()->loginPath !== false) { + if (Cms::config()->getLoginPath() !== false) { Route::get(CpAuthPath::TwoFactorChallenge->value, [TwoFactorAuthenticationController::class, 'showForm']); } diff --git a/tests/Feature/Http/Controllers/Auth/OAuthControllerTest.php b/tests/Feature/Http/Controllers/Auth/OAuthControllerTest.php index faa8724d202..d07640c121d 100644 --- a/tests/Feature/Http/Controllers/Auth/OAuthControllerTest.php +++ b/tests/Feature/Http/Controllers/Auth/OAuthControllerTest.php @@ -65,8 +65,12 @@ function oauthControllerCallbackUrl(bool $isCpRequest = false, string $provider function oauthControllerLoginUrl(bool $isCpRequest): string { - if (! $isCpRequest && app(GeneralConfig::class)->loginPath !== false) { - return Url::siteUrl(app(GeneralConfig::class)->getLoginPath()); + if (! $isCpRequest) { + $loginPath = app(GeneralConfig::class)->getLoginPath(); + + if ($loginPath !== false) { + return Url::siteUrl($loginPath); + } } return cp_url('login');