fix(register): don't block signup on unrecognized browser timezone (#462)

## Problem

Some users reported they couldn't register: they filled the form
correctly, pressed the button, saw a loading spinner, then got bounced
back to the registration page with **no error message**. Meanwhile 20-30
users register fine every day, and the issue wasn't reproducible by the
team.

## Root cause

The register form sends a **hidden, browser-auto-detected `timezone`**
field (`Intl.DateTimeFormat().resolvedOptions().timeZone`). The backend
validated it with Laravel's `timezone` rule:

```php
'timezone' => ['nullable', 'string', 'timezone'],
```

That rule rejects **legacy IANA aliases** that many browsers/OSes still
emit — e.g. `Asia/Calcutta`, `Asia/Saigon`, `Asia/Rangoon`,
`US/Pacific`. Proof on PHP 8.4:

```
in_array("Asia/Calcutta", DateTimeZone::listIdentifiers())  →  false
```

When validation failed, Fortify redirected back with a `timezone` error
— but the form had **no error display** for timezone (every other field
has one). Result: spinner → silent bounce → no message → and the user
can't fix a hidden field anyway. The team couldn't reproduce because
their browsers send canonical zones that pass. Production tzdata may
reject even more zones.

## Fix

- Stop validating the auto-detected hidden field against the strict
timezone list.
- Add `normalizeTimezone()`: keep recognized zones (including BC aliases
via `DateTimeZone::ALL_WITH_BC`), drop anything unrecognized to `null`.
Registration can never be blocked by it again.
- Surface `errors.timezone` in the form as a safety net so a
hidden-field failure can never be silent in the future.

## Tests

- Legacy alias `Asia/Calcutta` → registers, stored as-is.
- Unrecognized zone → registers, stored `null`.
- All existing registration tests pass (13 passed).
This commit is contained in:
Víctor Falcón 2026-06-01 09:03:15 +02:00 committed by GitHub
parent a71626a350
commit 96ee311299
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 57 additions and 2 deletions

View File

@ -35,7 +35,7 @@ class CreateNewUser implements CreatesNewUsers
Rule::unique(User::class),
],
'password' => $this->passwordRules(),
'timezone' => ['nullable', 'string', 'timezone'],
'timezone' => ['nullable', 'string', 'max:255'],
])->validate();
$user = User::create([
@ -43,7 +43,7 @@ class CreateNewUser implements CreatesNewUsers
'email' => $input['email'],
'password' => $input['password'],
'locale' => $this->detectLocaleFromRequest(),
'timezone' => $input['timezone'] ?? null,
'timezone' => $this->normalizeTimezone($input['timezone'] ?? null),
]);
if (! config('mail.email_verification_enabled')) {
@ -53,6 +53,23 @@ class CreateNewUser implements CreatesNewUsers
return $user;
}
/**
* Normalize a browser-detected timezone, discarding identifiers PHP does
* not recognize so a hidden auto-detected field can never block registration.
*/
protected function normalizeTimezone(?string $timezone): ?string
{
if ($timezone === null || $timezone === '') {
return null;
}
if (! in_array($timezone, timezone_identifiers_list(\DateTimeZone::ALL_WITH_BC), true)) {
return null;
}
return $timezone;
}
/**
* Detect locale from Accept-Language header.
*/

View File

@ -69,6 +69,8 @@ export default function Register({
value={detectedTimezone}
/>
<InputError message={errors.timezone} />
<div className="grid gap-6">
<div className="grid gap-2">
<Label htmlFor="name">{__('Name')}</Label>

View File

@ -93,6 +93,42 @@ test('new users store their detected timezone on registration', function () {
expect($user->timezone)->toBe('America/New_York');
});
test('new users can register with a legacy timezone alias', function () {
Queue::fake();
$response = $this->post(route('register.store'), [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password',
'password_confirmation' => 'password',
'timezone' => 'Asia/Calcutta',
]);
$this->assertAuthenticated();
$response->assertRedirect(route('onboarding', absolute: false));
expect(User::where('email', 'test@example.com')->first()->timezone)
->toBe('Asia/Calcutta');
});
test('new users can register when the browser sends an unrecognized timezone', function () {
Queue::fake();
$response = $this->post(route('register.store'), [
'name' => 'Test User',
'email' => 'test@example.com',
'password' => 'password',
'password_confirmation' => 'password',
'timezone' => 'Not/AZone',
]);
$this->assertAuthenticated();
$response->assertRedirect(route('onboarding', absolute: false));
expect(User::where('email', 'test@example.com')->first()->timezone)
->toBeNull();
});
test('new users can register without a timezone', function () {
Queue::fake();