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:
parent
a71626a350
commit
96ee311299
|
|
@ -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.
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue