Разработка
July 15

Laravel Livewire — вход по имени пользователя или электронной почте

Если поставить Laravel Starter Kit с Livewire внутри, который мы в прошлый раз немного поизучали, то у нас сразу, из коробки будет поддержка аутентификации. Но — только по электронной почте и паролю. А заказчики могут хотеть (и хотят) чтобы можно было входить с помощью логина, либо вместо почты, либо в качестве альтернативы. Причины могут быть разные, не будем зацикливаться. Вместо этого давайте посмотрим, как сделать, чтобы можно было использовать и логин, и почту в качестве идентификатора пользователя при входе.

Начнём как положено, с тестов. В tests/Feature/Auth у нас есть AuthenticationTest.php, в котором уже находятся дефолтные тесты аутентификации. Выберем главный опорный тест, на базе которого проведем изменения:

public function test_users_can_authenticate_using_the_login_screen(): void
{
    $user = User::factory()->create();

    $response = LivewireVolt::test('auth.login')
        ->set('email', $user->email)
        ->set('password', 'password')
        ->call('login');

    $response
        ->assertHasNoErrors()
        ->assertRedirect(route('platform.main', absolute: false));

    $this->assertAuthenticated();
}

мы видим что тест проверяет, что пользователь может задать на экране логина свой мейл и пароль, и тогда он войдет на сайт. Допустим, новое поле ввода (вместо почты) у нас будет называться login и мы хотим использовать его как для почты, так и для имени пользователя. В качестве имени будем использовать $user->name, который тоже есть из коробки.

Изменим наш тест так, чтобы получилось два:

public function test_users_can_authenticate_using_the_login_screen(): void
{
    $user = User::factory()->create();

    $response = LivewireVolt::test('auth.login')
        ->set('login', $user->email)
        ->set('password', 'password')
        ->call('login');

    $response
        ->assertHasNoErrors()
        ->assertRedirect(route('platform.main', absolute: false));

    $this->assertAuthenticated();
}

    
public function test_users_can_authenticate_with_name(): void
{
    $user = User::factory()->create();

    $response = LivewireVolt::test('auth.login')
        ->set('login', $user->name)
        ->set('password', 'password')
        ->call('login');

    $response
        ->assertHasNoErrors()
        ->assertRedirect(route('platform.main', absolute: false));

    $this->assertAuthenticated();
}

У нас есть два теста, проверяющие, что если пользователь введет в поле login свое имя или почту, а также верный пароль, он успешно войдет на сайт. Однако пока эта логика не реализована. Реализуем её.

Все изменения будут сосредоточены в одном Volt-компоненте resources/views/livewire/auth/login.blade.php. Поэтому приведу сразу измененный код компонента с комментариями, где и что поменяли:

<?php

// ...

new #[Layout('components.layouts.auth')] class extends Component {
	// Изменим свойство $email на $login, уберем email из аннотации Validate, т.к. теперь можно ввести не только почту
	#[Validate('required|string')]
	public string $login = '';

	// ...

	/**
	* Handle an incoming authentication request.
	*/
	// Мы переименовали метод login() в authenticate(), чтобы не было конфликта с полем $login. 
	// Если этого не сделать, компонент забуксует и работать не будет.
	public function authenticate(): void
	{
		$this->validate();
	 
		$this->ensureIsNotRateLimited();
	
		// Определим, ввёл пользователь email или имя, для этого попробуем проверить $this->login фильтром по email.
		// Если это имейл, в массиве $credentials укажем ключ email, иначе укажем name.
		$credentials = filter_var($this->login, FILTER_VALIDATE_EMAIL)
			? ['email' => $this->login, 'password' => $this->password]
			: ['name' => $this->login, 'password' => $this->password];

		// и передадим массив $credentials для авторизации
		if (!Auth::attempt($credentials, $this->remember)) {
			RateLimiter::hit($this->throttleKey()); 
			throw ValidationException::withMessages([
				// Здесь мы тоже поменяли ключ с email на login, поскольку инпута email в форме логина больше нет.
				'login' => __('auth.failed'),
			]);
		}

		// ...
	}

	// ...

	/**
	* Get the authentication rate limiting throttle key.
	*/
	protected function throttleKey(): string
	{
		// Тут тоже поменяли поле email на login
		return Str::transliterate(Str::lower($this->login) . '|' . request()->ip());
	}
	
}; ?>

А в вёрстке компонента мы просто меняем инпут имейла на login (меняем атрибут type, убираем autocomplete):

	<!-- ... -->
	<form wire:submit="authenticate" class="flex flex-col gap-6">
		<!-- Login -->
		<flux:input
			wire:model="login"
			:label="'Логин или email'"
			type="text"
			required
			autofocus
			placeholder="Имя пользователя или email@example.com"
		/>
		<!-- ... -->

Да, и поскольку теперь мы используем для входа $user->name, нужно сделать поле уникальным (в дефолтной миграции этого нет). Нужно создать новую миграцию и добавить в метод up() код:

Schema::table('users', function (Blueprint $table) {
	$table->unique('name');
});

Также стоит добавить 'unique:' . User::class в правила валидации компонента resources/views/livewire/auth/register.blade.php, чтобы юзеры не видели ошибку http 500, когда введут имя, которое уже есть. Ну и если вы используете еще какие-то формы создания или редактирования пользователя, там тоже стоит добавить валидацию на уникальность.

Напоследок запустите все тесты в AuthenticationTest.php и исправить email на login в оставшихся тестах, чтобы они проходили.