fix(auth): remove dummy login route, validate locale allowlist

- Remove no-op POST /login route that caused named-route collision and
  was testing throttle:5,1 middleware against a null handler.
- Rewrite LoginRateLimitTest to drive the real Volt login component via
  Volt::test(), verifying the RateLimiter blocks the 6th attempt with
  auth.throttle message; throttle is Laravel core, test confirms wiring.
- Add supported_locales allowlist to config/app.php ['de', 'en'].
- LocaleFromUser middleware now validates user locale against allowlist
  and falls back to app.locale for unknown/unsupported values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
boban 2026-05-16 00:38:32 +02:00
parent 9d5d5af24d
commit ee0824302a
4 changed files with 35 additions and 8 deletions

View File

@ -11,7 +11,11 @@ class LocaleFromUser
public function handle(Request $request, Closure $next) public function handle(Request $request, Closure $next)
{ {
if ($user = $request->user()) { if ($user = $request->user()) {
App::setLocale($user->locale ?? config('app.locale')); $supported = config('app.supported_locales', [config('app.locale')]);
$locale = in_array($user->locale, $supported, true)
? $user->locale
: config('app.locale');
App::setLocale($locale);
} }
return $next($request); return $next($request);
} }

View File

@ -80,6 +80,8 @@ return [
'locale' => env('APP_LOCALE', 'en'), 'locale' => env('APP_LOCALE', 'en'),
'supported_locales' => ['de', 'en'],
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'), 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'), 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),

View File

@ -11,8 +11,6 @@ Route::middleware('guest')->group(function () {
Volt::route('login', 'pages.auth.login') Volt::route('login', 'pages.auth.login')
->name('login'); ->name('login');
Route::post('login', fn () => null)->middleware('throttle:5,1')->name('login.post');
Volt::route('forgot-password', 'pages.auth.forgot-password') Volt::route('forgot-password', 'pages.auth.forgot-password')
->name('password.request'); ->name('password.request');

View File

@ -1,11 +1,34 @@
<?php <?php
it('throttles login after 5 failed attempts', function () { use Illuminate\Support\Facades\RateLimiter;
$this->withoutMiddleware(\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class); use Illuminate\Support\Str;
use Livewire\Volt\Volt;
it('throttles login after 5 failed attempts', function () {
$email = 'test@test.com';
// The throttle key LoginForm uses: Str::transliterate(Str::lower($email) . '|' . request()->ip())
// In the test environment request()->ip() returns '127.0.0.1'
$throttleKey = Str::transliterate(Str::lower($email) . '|127.0.0.1');
RateLimiter::clear($throttleKey);
// Make 5 failed attempts through the real Volt component
for ($i = 0; $i < 5; $i++) { for ($i = 0; $i < 5; $i++) {
$this->post('/login', ['email' => 'test@test.com', 'password' => 'wrong']); Volt::test('pages.auth.login')
->set('form.email', $email)
->set('form.password', 'wrong' . $i)
->call('login');
} }
$response = $this->post('/login', ['email' => 'test@test.com', 'password' => 'wrong']);
$response->assertStatus(429); // 6th attempt must be rate-limited — error on form.email with throttle message
$component = Volt::test('pages.auth.login')
->set('form.email', $email)
->set('form.password', 'wrong6')
->call('login');
$component->assertHasErrors(['form.email']);
$errors = $component->errors();
expect($errors->has('form.email'))->toBeTrue();
expect($errors->first('form.email'))->toContain('Too many login attempts');
}); });