Quando um novo usuário é cadastrado no sistema, normalmente é enviado um e-mail para o mesmo tendo em seu conteúdo um "link de ativação/verificação" para saber se o e-mail colocado no formulário de cadastro realmente pertence a ele. Isso é útil quando o fluxo de um novo cadastro não deve ser bloqueado quando o mesmo não clicar no link enviado por e-mail, assim, ele poderia se cadastrar e já utilizar a plataforma de imediato com algumas ressalvas acredito, por conta do seu cadastro não estar 100% completo (sem ter o e-mail validado nesse casso).
Existe outro caso, que é quando o sistema não permite seu uso sem antes ter validado o e-mail e/ou senha, quando o uso dos dados de contato faz parte do fluxo de "ativação da conta", ou seja, a conta do usuário só é considerada ativa, quando os dados de contato estiverem sidos validados, caso contrário, existe algum outro status como "pendente de ativação" por exemplo, e ai, não usaria 100% da plataforma por conta dessa "limitação" do próprio usuário em não validar os dados de contato. Dessa forma, fica fácil distinguir entre contas/registros que estão íntegros/consistentes, e aqueles que ainda estão faltando alguma coisa pra que sua conta/registro seja considerado "válida".
Resumo
- O nome da tabela que será utilizada para armazenar os registros dos usuários do sistema vai se chamar
users
😮, que terá todos os campos necessário para um novo registro do usuário.- Vamos adicionar 3 novos campos na tabela pra poder manipular a validação da verificação dos codes.
- O nome da coluna que armazenará o telefone celular é
phone
e a coluna que terá o e-mail éemail
.
- No fluxo abaixo, tanto o e-mail, quanto o telefone celular, devem ser satisfeitos, isso é, validados, para que a conta do usuário possa ser considerada como "ativa".
- O fluxo de validação e verificação se inicial após o signup do usuário. Após ele se registrar no sistema, vai ser disparado inicialmente o code para o seu telefone, na tela seguinte, vai pedir o code recém enviado, inserindo e estando válido, então vai ser enviado o próximo code por e-mail, e na tela seguinte inserindo e estando correto o code, então o usuário seria considerado como validado/íntegro/verificado.
- O serviço utilizado nesse artigo pra envio do SMS é o
Twilio
. - Tanto o envio do code por SMS quanto por e-mail será feito utilizando
Jobs
.
Primeiros Passos
Adicionando as novas colunas na tabela de users
A migration a seguir deve ser executada para que:
- Os codes para validação e verificação sejam salvos na coluna de
verification_codes
. - Quando o code do e-mail fosse validado com sucesso, então, a coluna de
email_verified_at
teria a data em que isso aconteceu. - Quando o code do telefone via SMS fosse validado com sucesso, então, a coluna de
phone_verified_at
teria a data em que isso aconteceu.
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->json('verification_codes')->after('password')->nullable();
$table->timestamp('email_verified_at')->after('email')->nullable();
$table->timestamp('phone_verified_at')->after('phone')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('verification_codes');
$table->dropColumn('email_verified_at');
$table->dropColumn('phone_verified_at');
});
}
};
Modificando a model de User
para manipular as novas colunas
A model de User
tem um dos principais métodos que é o saveVerificationCode
, responsável por criar e salvar os códigos de verificação na coluna de verification_codes
. Além do mais, métodos que recuperem os códigos smsCode
e emailCode
, e também o conceito de "ativação de conta do usuário" activateAccount
, e saber se a conta do usuário é considerada como "verificada" accountHasVerified
.
<?php
namespace App\Models;
use App\Enums\UserStatus;
use App\Notifications\UserVerifyEmail;
use Illuminate\Notifications\Notifiable;
use App\Enums\UserAccountTypeVerification;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable implements MustVerifyEmail
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'email',
'password',
'phone',
'verification_codes',
'email_verified_at',
'phone_verified_at',
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'verification_codes' => 'object',
'email_verified_at' => 'datetime',
'phone_verified_at' => 'datetime',
];
/**
* Retrieve common user data.
*
* @return array
*/
public function commonUserData(): array
{
return [
'id' => $this->getKey(),
'name' => $this->name,
'email' => $this->email,
'status' => $this->status,
'has_phone_verified' => $this->hasVerifiedPhone(),
'has_email_verified' => $this->hasVerifiedEmail(),
];
}
/**
* Send the email verification notification.
*
* @return void
*/
public function sendEmailVerificationNotification(): void
{
$this->notify(new UserVerifyEmail);
}
/**
* Create the verification code structure to be saved in the database.
*
* @param \App\Enums\UserAccountTypeVerification $type
* @param array|string|null $value
*
* @return int
*/
public function saveVerificationCode(UserAccountTypeVerification $type, array|string|null $value = null): int
{
$code = random_int(100000, 999999);
$data = [
UserAccountTypeVerification::SMS->value => null,
UserAccountTypeVerification::EMAIL->value => null,
];
$data[$type->value] = [
'code' => $code,
'value' => $value,
'created_at' => now(),
];
$this->fill([
'verification_codes' => $data,
])->save();
return $code;
}
/**
* Determine if the user has verified their email address.
*
* Checks if the Email code has been validated.
*
* @return bool
*/
public function hasVerifiedEmail(): bool
{
return ! is_null($this->email_verified_at);
}
/**
* Mark the given user's email as verified.
*
* @return bool
*/
public function markEmailAsVerified(): bool
{
return $this->forceFill([
'email_verified_at' => $this->freshTimestamp(),
])->save();
}
/**
* Determine whether the user has verified their phone number.
*
* Checks if the SMS code has been validated.
*
* @return bool
*/
public function hasVerifiedPhone(): bool
{
return ! is_null($this->phone_verified_at);
}
/**
* Mark the given user's phone number as verified.
*
* @return bool
*/
public function markPhoneAsVerified(): bool
{
return $this->forceFill([
'phone_verified_at' => $this->freshTimestamp(),
])->save();
}
/**
* Retrieves the user's SMS code.
*
* @return int|null
*/
public function smsCode(): ?int
{
return intval($this->verification_codes?->sms?->code) ?: null;
}
/**
* Retrieves the `value` column data from `sms`.
*
* @return mixed
*/
public function phoneData(): mixed
{
return $this->verification_codes?->sms?->value;
}
/**
* Retrieves the user's Email code.
*
* @return int|null
*/
public function emailCode(): ?int
{
return intval($this->verification_codes?->email?->code) ?: null;
}
/**
* Retrieves the `value` column data from `email`.
*
* @return mixed
*/
public function emailData(): mixed
{
return $this->verification_codes?->email?->value;
}
/**
* Reset the `verification codes` field.
*
* @return void
*/
public function resetVerificationCodes(): void
{
if (empty($this->emailCode()) && empty($this->smsCode())) {
$this->fill(['verification_codes' => null])->save();
}
}
/**
* Activate the user account.
*
* @return bool
*/
public function activateAccount(): bool
{
return $this->forceFill([
'status' => UserStatus::ENABLE,
'verification_codes' => null,
])->save();
}
/**
* Checks if the user account has been validated in all possible steps.
*
* @return bool
*/
public function accountHasVerified(): bool
{
return $this->status === UserStatus::ENABLE &&
$this->verification_codes === null;
}
}
O enum de UserStatus
tem o seguinte código:
<?php
namespace App\Enums;
enum UserStatus: string
{
case ENABLE = 'enable';
case DISABLE = 'disable';
}
O enum de UserAccountTypeVerification
tem o seguinte código:
<?php
namespace App\Enums;
enum UserAccountTypeVerification: string
{
case SMS = 'sms';
case EMAIL = 'email';
}
Configurando o Twilio
Para enviar o code com Twilio, é necessário instalar o pacote oficial para o PHP, que nesse caso é o twilio/sdk
. Após instalá-lo, crie um arquivo chamado twilio.php
na pasta de config
, tendo o seguinte conteúdo:
<?php
return [
'number' => env('TWILIO_NUMBER'),
'account_sid' => env('TWILIO_ACCOUNT_SID'),
'auth_token' => env('TWILIO_AUTH_TOKEN'),
];
As credenciais acima são recuperadas através do painel no dashboard na conta do Twilio.
Para usar a classe de Twilio\Rest\Client
, precisamos configurá-la no service container no Laravel, para favorecer o código com a inversão de dependência, deixando o contêiner do Laravel resolvendo a criação desse objeto, assim, temos todos as vantagens do princípio da inversão de dependência, além do mais, quando esse código for testado, e precisar simular, e saber se realmente o método está sendo chamada (testes de spy) ou mockar determinado método do Client pra ter determinado comportamento esperado pelo código, será beeemmmm mais fácil, pois novamente, estamos deixando a responsabilidade da instância desse objeto para o Container do Laravel, e não tendo um alto acoplamento usando new
diretamente, que nesse caso, não seria possível simular/mockar new
nos testes.
Na classe de AppServiceProvider
no método de register
adicione o seguinte código:
use Twilio\Rest\Client;
use App\Support\APIs\SMSClient;
use App\Support\APIs\TwilioClient;
$this->app->bind(Client::class, fn () => new Client(config('twilio.account_sid'), config('twilio.auth_token')));
$this->app->bind(SMSClient::class, TwilioClient::class);
Agora vamos encapsular toda as chamadas ao serviço do Twilio em uma classe com nomes de métodos legíveis ao nosso código, fazendo um contrato entre essa nova classe e o código da aplicação, assim, quando o Client do Twilio mudar, não precisamos refatorar a aplicação em vários locais (se tiver usando diretamente o Client do Twilio ao invés da estratégia abaixo), precisa ser feito em apenas um único local/método, pois toda responsabilidade não fica mais disperso na aplicação, e sim em uma classe encapsulada/responsável por ser uma espécie de "proxy" basicamente entre o Client do Twilio e o código da aplicação. Então, a classe é TwilioClient
que terá apenas o método send
, que saberá como enviar o SMS para o usuário. Além do mais, se no futuro o projeto decidir mudar do Twilio para alguma outra ferramenta, é só novamente configurar no Container do Laravel mudando a classe concreta na resolução de $this->app->bind(SMSClient::class, TwilioClient::class);
.
<?php
namespace App\Support\APIs;
use Twilio\Rest\Client;
class TwilioClient implements SMSClient
{
private readonly string $twilioNumber;
private const BR_DDI = '+55';
public function __construct(private Client $twilio)
{
$this->twilioNumber = config('twilio.number');
}
public function send(string $toPhone, string $message): void
{
$this->twilio->messages->create(static::BR_DDI . $toPhone, [
'from' => $this->twilioNumber,
'body' => $message,
]);
}
}
A interface de SMSClient
tem apenas o contrato do método de send
:
<?php
namespace App\Support\APIs;
interface SMSClient
{
public function send(string $toPhone, string $message): void;
}
Organizando as rotas
As seguintes rotas, servem para exemplificar o fluxo que acontecerá, que é:
- O usuário se cadastra na rota de
signup
, e nesse mesmo endpoint/código, após um novo registro ser inserido na tabela deusers
, então, é disparado o primeiro Job (SendUserVerificationCodeBySMS
), que enviará o code por SMS para o telefone celular do usuário inserido no formulário de cadastro. - Após ter recebido o code via SMS no telefone, é então validado na rota de
account.verify
no endpoint dePOST /account/verify
.- Após o code estar válido, então, será executado o segundo Job que nesse caso é pra enviar o code via e-mail (
SendUserVerificationCodeByEmail
).
- Após o code estar válido, então, será executado o segundo Job que nesse caso é pra enviar o code via e-mail (
- O usuário recebendo o code no seu e-mail que colocou no formulário de cadastro, novamente deve ser validado em
POST /account/verify
. - O code do e-mail estando válido, então, o callback pode ser o que o negócio determina. Pode ser alterado o status do usuário, pode ser feito várias coisas, isso quem vai decidir é o negócio.
- Se por acaso o usuário fechou a aplicação ou precisar reenviar o code por SMS ou e-mail, então, pode ser usando o endpoint de
POST /account/verify/resend
.
Route::controller(RegisterController::class)->group(function () {
Route::post('signup', 'signup')->middleware('guest')->name('signup');
Route::middleware('auth')->name('logged.')->group(function () {
Route::post('account/verify', 'accountVerify')->name('account.verify');
Route::post('account/verify/resend', 'accountVerifyResend')->name('account.verify.resend');
});
});
Criando Job para enviar code por SMS usando Twilio
Após o usuário se cadastrar, é enviado um code por SMS para o seu telefone celular colocado no formulário de cadastro, então, no mesmo fluxo/código/método em que um novo usuário é cadastrado, deve ser feito o envio do code por SMS ou por E-mail, no nosso caso, primeiro é validado o telefone celular, e após isso, recebendo o code via SMS e validando-o, então, é enviado o code para seu e-mail.
O primeiro Job que será disparado para notificar o usuário via SMS pra que o mesmo posso estar validando seu telefone celular é o SendUserVerificationCodeBySMS
, com o seguinte código:
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use App\Support\APIs\TwilioClient;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use App\Enums\UserAccountTypeVerification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class SendUserVerificationCodeBySMS implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @param \App\Models\User $user
* @param string|null $phone
*/
public function __construct(public User $user, public ?string $phone = null)
{
//
}
/**
* Execute the job.
*
* @param \App\Support\APIs\TwilioClient $twilioClient
*
* @return void
*/
public function handle(TwilioClient $twilioClient): void
{
$phone = ($this->phone ?? $this->user->phone);
$code = $this->user->saveVerificationCode(UserAccountTypeVerification::SMS, [
'phone' => $phone,
]);
$message = "$code is your verification code";
$twilioClient->send($phone, $message);
}
}
O job acima pode ser executado em um endpoint por exemplo de POST /signup
em um controller de RegisterController
, no método de signup
, como no código abaixo:
public function signup(AuthSignupRequest $request): Response
{
DB::beginTransaction();
try {
$user = $this->userService->signup($request->safe());
SendUserVerificationCodeBySMS::dispatch($user);
DB::commit();
} catch (Exception $e) {
DB::rollBack();
throw $e;
}
return response()->json(['user' => $user->commonUserData()]);
}
Criando Job para enviar code via e-mail
O Job a seguir será executado no segundo passo, quando o usuário já tiver validado o telefone celular, e precisará validar seu e-mail:
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use App\Enums\UserAccountTypeVerification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
class SendUserVerificationCodeByEmail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*
* @param \App\Models\User $user
* @param string|null $email
*
* @return void
*/
public function __construct(public User $user, public ?string $email = null)
{
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
$this->user->email = $this->email ?? $this->user->email;
$this->user->saveVerificationCode(UserAccountTypeVerification::EMAIL, $this->user->email);
$this->user->sendEmailVerificationNotification();
}
}
O método de sendEmailVerificationNotification
na model de User
é para enviar uma notificação via e-mail, que nesse caso é a UserVerifyEmail
com o seguinte código:
<?php
namespace App\Notifications;
use Illuminate\Notifications\Notification;
use Illuminate\Notifications\Messages\MailMessage;
class UserVerifyEmail extends Notification
{
/**
* Get the notification's channels.
*
* @param mixed $notifiable
*
* @return array
*/
public function via($notifiable): array
{
return ['mail'];
}
/**
* Build the mail representation of the notification.
*
* @param mixed $notifiable
*
* @return \Illuminate\Notifications\Messages\MailMessage
*/
public function toMail($notifiable): MailMessage
{
$code = $notifiable->emailCode();
return (new MailMessage)
->subject('Your verification code')
->greeting('See your verification code')
->line('Please open the app on your mobile and validate your email by entering the code below.')
->line('Use the following verification code: ' . $code);
}
}
Validando o code
Para validar o code tanto por SMS quanto por e-mail é o mesmo endpoint, mesmo body, o que muda é o tipo de validação, ou nesse caso, o valor do chave de type
no body da requisição.
Mais primeiro, deve ser criado o FormRequest
que valide corretamente o POST
da request de validação do code, nesse caso, o arquivo de AuthAccountVerifyRequest
.
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rules\Enum;
use App\Enums\UserAccountTypeVerification;
use Illuminate\Foundation\Http\FormRequest;
class AuthAccountVerifyRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'type' => ['required', new Enum(UserAccountTypeVerification::class)],
'code' => ['required', 'integer'],
];
}
/**
* Retrieves account verification type.
*
* @return \App\Enums\UserAccountTypeVerification
*/
public function type(): UserAccountTypeVerification
{
return UserAccountTypeVerification::from($this->input('type'));
}
}
Agora, vamos criar uma classe chamada UserService
pra ser um serviço responsável por manipular tudo aquilo que for relacionado ao usuário, contendo inicialmente os métodos de accountVerify
, verifySMSCode
e verifyEmailCode
, com o seguinte código:
<?php
namespace App\Services;
use App\Models\User;
use App\Enums\UserAccountTypeVerification;
use App\Jobs\SendUserVerificationCodeByEmail;
use App\Services\Exceptions\InvalidSMSCodeException;
use App\Services\Exceptions\InvalidEmailCodeException;
use App\Services\Exceptions\AccountHasAlreadyVerifiedException;
class UserService
{
public function __construct(User $model)
{
}
/**
* Process to validate customer account verification.
*
* @param \App\Models\User $user
* @param int $code
* @param \App\Enums\UserAccountTypeVerification $type
*
* @return void
*/
public function accountVerify(User $user, int $code, UserAccountTypeVerification $type): void
{
if ($user->accountHasVerified()) {
throw new AccountHasAlreadyVerifiedException;
}
if ($this->verifySMSCode($user, $code, $type)) {
SendUserVerificationCodeByEmail::dispatch($user);
return;
}
$this->verifyEmailCode($user, $code, $type);
// Aqui pode ser feito alguma ação de negócio quando ambos os steps (SMS/Telefone e E-mail)
// forem validados com sucesso, nesse caso, o code está "ativando" a conta do usuário que
// outrora era considerada como "pendente de ativação"
$user->activateAccount();
}
/**
* Checks the user code sent via SMS.
*
* @param \App\Models\User $user
* @param int $code
* @param \App\Enums\UserAccountTypeVerification $type
*
* @return bool|null
*/
public function verifySMSCode(User $user, int $code, UserAccountTypeVerification $type): ?bool
{
if ($type === UserAccountTypeVerification::SMS) {
if ($user->smsCode() !== $code) throw new InvalidSMSCodeException;
if (! $user->hasVerifiedPhone()) $user->markPhoneAsVerified();
return true;
}
return null;
}
/**
* Check the user code sent via Email.
*
* @param \App\Models\User $user
* @param int $code
* @param \App\Enums\UserAccountTypeVerification $type
*
* @return bool|null
*/
public function verifyEmailCode(User $user, int $code, UserAccountTypeVerification $type): ?bool
{
if ($type === UserAccountTypeVerification::EMAIL) {
if ($user->emailCode() !== $code) throw new InvalidEmailCodeException;
if (! $user->hasVerifiedEmail()) $user->markEmailAsVerified();
return true;
}
return null;
}
}
No controller de RegisterController
, adicione o método de accountVerify
, com o seguinte código:
<?php
namespace App\Http\Controllers\Auth;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Foundation\Bus\DispatchesJobs;
use App\Http\Requests\AuthAccountVerifyRequest;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Routing\Controller as LaravelBaseController;
class RegisterController extends LaravelBaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
/**
* Create a new Controller instance.
*
* @param \App\Services\UserService $userService
*/
public function __construct(private UserService $userService)
{
}
/**
* Process to validate customer account verification.
*
* @param \App\Http\Requests\AuthAccountVerifyRequest $request
*
* @return \Illuminate\Http\JsonResponse
*
* @throws \App\Services\Exceptions\InvalidSMSCodeException
* @throws \App\Services\Exceptions\InvalidEmailCodeException
* @throws \App\Services\Exceptions\AccountHasAlreadyVerifiedException
*/
public function accountVerify(AuthAccountVerifyRequest $request): JsonResponse
{
$this->userService->accountVerify(auth()->user(), $request->code, $request->type());
return response()->json(status: JsonResponse::HTTP_NO_CONTENT);
}
}
Pronto, agora é só enviar a requisição com o payload de:
{
"type": "sms or email",
"code": 123456
}
Onde o type
pode ser sms
ou email
.
Reenviado o code
Se o usuário demorou muito pra validar o code, ou existe alguma coisa de "skyp" naquele momento da aplicação pra que ele possa fazer isso depois, então, o código abaixo é exatamente pra isso, pra enviar o code via SMS ou e-mail em qualquer momento (de acordo com as regras do negócio).
Primeiro, adicione o método de accountVerifyResend
na controller RegisterController
:
/**
* Process to resend customer account verification code.
*
* @param \App\Http\Requests\AuthAccountVerifyResendRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function accountVerifyResend(AuthAccountVerifyResendRequest $request): JsonResponse
{
$user = auth()->user();
$this->userService->accountVerifyResend($user, $request->type());
return response()->json(status: JsonResponse::HTTP_NO_CONTENT);
}
Crie um FormRequest
chamado de AuthAccountVerifyResendRequest
tendo o seguinte código:
<?php
namespace App\Http\Requests;
use Illuminate\Validation\Rules\Enum;
use App\Enums\UserAccountTypeVerification;
use App\Support\Http\Requests\BaseRequest;
class AuthAccountVerifyResendRequest extends BaseRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules(): array
{
return [
'type' => ['required', new Enum(UserAccountTypeVerification::class)],
];
}
/**
* Retrieves account verification type.
*
* @return \App\Enums\UserAccountTypeVerification
*/
public function type(): UserAccountTypeVerification
{
return UserAccountTypeVerification::from($this->input('type'));
}
}
Em seguida, adicione o método de accountVerifyResend
no serviço de UserService
:
/**
* Process to resend verification account to customer.
*
* @param \App\Models\User $user
* @param \App\Enums\UserAccountTypeVerification $type
*
* @return void
*/
public function accountVerifyResend(User $user, UserAccountTypeVerification $type): void
{
if ($type === UserAccountTypeVerification::SMS) {
SendUserVerificationCodeBySMS::dispatch($user);
} elseif ($type === UserAccountTypeVerification::EMAIL) {
SendUserVerificationCodeByEmail::dispatch($user);
}
}
Agora é só enviar a request com o seguinte payload:
{
"type": "sms or email"
}
Bônus: "Esqueci minha senha" - enviando code via SMS ou e-mail
Para que o usuário posso resetar sua senha (esqueci minha senha), podemos enviar o code para seu telefone celular via SMS, ou para seu e-mail, para que o seu registro no banco de dados possa ser recuperado e manipulado corretamente. Pra isso, é necessário primeiro configurar as rotas necessárias para tais ações:
Route::controller(ForgotController::class)->middleware('guest')->group(function () {
Route::post('forgot-password/send-code', 'sendCode')->name('forgot-password.send-code');
Route::post('forgot-password/validate-code', 'validateCode')->name('forgot-password.validate-code');
Route::post('reset-password', 'resetPassword')->name('forgot-password.password-update');
});
O controller de ForgotController
é responsável por tudo que envolve o "recuperar senha", e tem o seguinte código:
<?php
namespace App\Http\Controllers\Auth;
use App\Services\UserService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Validation\ValidationException;
use App\Http\Requests\AuthResetPasswordRequest;
use App\Http\Requests\AuthForgotPasswordRequest;
use Illuminate\Routing\Controller as LaravelBaseController;
use App\Http\Requests\AuthForgotPasswordValidateCodeRequest;
class ForgotController extends LaravelBaseController
{
/**
* Create a new Controller instance.
*
* @param \App\Services\UserService $userService
*/
public function __construct(private UserService $userService)
{
}
/**
* Send a reset code to the given user.
*
* @param \App\Http\Requests\AuthForgotPasswordRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function sendCode(AuthForgotPasswordRequest $request): JsonResponse
{
$user = $this->userService->forgotPasswordSendCode($request->type(), $request->input('value'));
$token = null;
$callbackResetLink = (function ($user, $newToken) use (&$token) {
$token = $newToken;
// $user->sendPasswordResetNotification($token);
});
// We will send the password reset link to this user. Once we have attempted
// to send the link, we will examine the response then see the message we
// need to show to the user. Finally, we'll send out a proper response.
$status = Password::broker()->sendResetLink(['email' => $user->getEmailForPasswordReset()], $callbackResetLink);
if ($status === Password::RESET_LINK_SENT) {
return response()->json(['user' => ['email' => $user->getEmailForPasswordReset()], 'token' => $token]]);
}
throw ValidationException::withMessages(['email' => __($status)]);
}
/**
* Validates the code previously registered.
*
* @param \App\Http\Requests\AuthForgotPasswordValidateCodeRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function validateCode(AuthForgotPasswordValidateCodeRequest $request): JsonResponse
{
$this->userService->forgotPasswordValidateCode($request->type(), $request->code, $request->email);
return response()->json(status: JsonResponse::HTTP_NO_CONTENT);
}
/**
* Reset the user's password.
*
* @param \App\Http\Requests\AuthResetPasswordRequest $request
*
* @return \Illuminate\Http\JsonResponse
*/
public function resetPassword(AuthResetPasswordRequest $request): JsonResponse
{
$token = '';
// Here we will attempt to reset the user's password. If it is successful we
// will update the password on an actual user model and persist it to the
// database. Otherwise we will parse the error and return the response.
$status = Password::broker()->reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, $password) use (&$token) {
$user->forceFill(['password' => Hash::make($password)])->save();
event(new PasswordReset($user));
auth()->login($user);
}
);
if ($status === Password::PASSWORD_RESET) {
return redirect()->intended('dashboard');
}
throw ValidationException::withMessages(['email' => [__($status)]]);
}
}
Adicione o seguinte código a Model de User
:
/**
* Get the e-mail address where password reset links are sent.
*
* @return string
*/
public function getEmailForPasswordReset(): string
{
return $this->email;
}
Os FormRequest
de AuthResetPasswordRequest
, AuthForgotPasswordRequest
e AuthForgotPasswordValidateCodeRequest
tem os seguintes códigos:
AuthResetPasswordRequest.php
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Validation\Rules\Password;
use Illuminate\Foundation\Http\FormRequest;
class AuthResetPasswordRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'token' => 'required',
'email' => [
'required',
'email',
'exists:' . User::class
],
'password' => [
'required',
Password::min(8)->letters()->numbers(),
'confirmed'
],
];
}
}
AuthForgotPasswordRequest.php
<?php
namespace App\Http\Requests;
use App\Models\User;
use App\Enums\UserStatus;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use App\Enums\UserAccountTypeVerification;
use Illuminate\Foundation\Http\FormRequest;
class AuthForgotPasswordRequest extends FormRequest
{
private array $valueRules = ['required'];
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
if (! $this->filled('type')):
return;
endif;
if ($this->type() === UserAccountTypeVerification::SMS) {
$this->valueRules = array_merge($this->valueRules, [Rule::exists(User::class, 'phone')]);
} elseif ($this->type() === UserAccountTypeVerification::EMAIL) {
$this->valueRules = array_merge($this->valueRules, [Rule::exists(User::class, 'email')]);
}
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'type' => ['required', new Enum(UserAccountTypeVerification::class)],
'value' => $this->valueRules,
];
}
/**
* Retrieves account verification type.
*
* @return \App\Enums\UserAccountTypeVerification
*/
public function type(): UserAccountTypeVerification
{
return UserAccountTypeVerification::from($this->input('type'));
}
}
AuthForgotPasswordValidateCodeRequest.php
<?php
namespace App\Http\Requests;
use App\Models\User;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use App\Enums\UserAccountTypeVerification;
use Illuminate\Foundation\Http\FormRequest;
class AuthForgotPasswordValidateCodeRequest extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* @return array<string, mixed>
*/
public function rules()
{
return [
'type' => ['required', new Enum(UserAccountTypeVerification::class)],
'code' => ['required', 'integer', 'regex:/^\d{6}$/'],
'email' => ['required', Rule::exists(User::class)],
];
}
/**
* Retrieves account verification type.
*
* @return \App\Enums\UserAccountTypeVerification
*/
public function type(): UserAccountTypeVerification
{
return UserAccountTypeVerification::from($this->input('type'));
}
}
Adicione o seguinte código ao serviço de UserService.php
:
/**
* Send password reset code to user's phone or email.
*
* @param \App\Enums\UserAccountTypeVerification $type
* @param string $value
*
* @return \App\Models\User
*/
public function forgotPasswordSendCode(UserAccountTypeVerification $type, string $value): User
{
if ($type === UserAccountTypeVerification::SMS) {
/** @var \App\Models\User */
$user = $this->model->getByPhone($type, $phone);
} else {
/** @var \App\Models\User */
$user = $this->model->getByEmail($type, $email);
}
$this->accountVerifyResend($user, $type);
return $user;
}
/**
* Checks and validates code confirmation.
*
* @param \App\Enums\UserAccountTypeVerification $type
* @param int $code
* @param string $email
*
* @throws \App\Services\Exceptions\InvalidEmailCodeException
* @throws \App\Services\Exceptions\InvalidSMSCodeException
*
* @return void
*/
public function forgotPasswordValidateCode(UserAccountTypeVerification $type, int $code, string $email): void
{
/** @var \App\Models\User */
$user = $this->repository->whereEmail($email)->firstOrFail();
$this->verifySMSCode($user, $code, $type);
$this->verifyEmailCode($user, $code, $type);
$user->fill(['verification_codes' => null])->save();
}
Dessa forma, será capaz de recuperar/resetar a senha usando codes de verificação para atualizar o registro do usuário dono do telefone celular ou e-mail, já que esses dois itens são exclusívos/únicos por pessoal.