Laravel 5.2 Email Verification with Activation Code

You can do this on two ways:

  • not allow to login at all
  • allow with reduced access

I will cover the first case.

Activation Email Received

In this example I will add mail confirmation on top of the Laravel's default auth scaffolding. It can be applied even if you already have working application.

If starting from scratch create a new laravel project and do the basic auth scaffold.

php artisan make:auth

Create a table which will hold all activation codes.

php artisan make:migration create_user_activations_table --create=user_activations

With these columns

Schema::create('user_activations', function (Blueprint $table) {
    $table->integer('user_id')->unsigned();
    $table->string('token')->index();
    $table->timestamp('created_at');
});

Add one more column to the users table (create new migration or add it to the existing one)

$table->boolean('activated')->default(false);

And start migrations

php artisan migrate

Next, we need ActivationRepository which will handle activation codes in database. Method getToken is taken from Illuminate/Auth/Passwords/DatabaseTokenRepository class. Other methods will be used in ActivationService.

<?php
namespace App;


use Carbon\Carbon;
use Illuminate\Database\Connection;

class ActivationRepository
{

    protected $db;

    protected $table = 'user_activations';

    public function __construct(Connection $db)
    {
        $this->db = $db;
    }

    protected function getToken()
    {
        return hash_hmac('sha256', str_random(40), config('app.key'));
    }

    public function createActivation($user)
    {

        $activation = $this->getActivation($user);

        if (!$activation) {
            return $this->createToken($user);
        }
        return $this->regenerateToken($user);

    }

    private function regenerateToken($user)
    {

        $token = $this->getToken();
        $this->db->table($table)->where('user_id', $user->id)->update([
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }

    private function createToken($user)
    {
        $token = $this->getToken();
        $this->db->table($this->table)->insert([
            'user_id' => $user->id,
            'token' => $token,
            'created_at' => new Carbon()
        ]);
        return $token;
    }

    public function getActivation($user)
    {
        return $this->db->table($this->table)->where('user_id', $user->id)->first();
    }


    public function getActivationByToken($token)
    {
        return $this->db->table($this->table)->where('token', $token)->first();
    }

    public function deleteActivation($token)
    {
        $this->db->table($this->table)->where('token', $token)->delete();
    }

}

Now define route to handle request when user clicks on the activation link. Add to routes.php

Route::get('user/activation/{token}', 'Auth\AuthController@activateUser')->name('user.activate');

It is best to put it under the Route::auth().

Now create ActivationService which will wrap logic for creating activation codes and sending emails. Two main methods are sendActivationMail and activateUser which are self explanatory.

Method shouldSend is checking if email is already sent, or it is sent recently. Property $resendAfter is number of hours that needs to pass before we send a now activation email but only if user request it. You will probably want to change default email sending function to use html template. Replace ->raw with ->send.

<?php

namespace App;


use Illuminate\Mail\Mailer;
use Illuminate\Mail\Message;

class ActivationService
{

    protected $mailer;

    protected $activationRepo;

    protected $resendAfter = 24;

    public function __construct(Mailer $mailer, ActivationRepository $activationRepo)
    {
        $this->mailer = $mailer;
        $this->activationRepo = $activationRepo;
    }

    public function sendActivationMail($user)
    {

        if ($user->activated || !$this->shouldSend($user)) {
            return;
        }

        $token = $this->activationRepo->createActivation($user);

        $link = route('user.activate', $token);
        $message = sprintf('Activate account <a href="%s">%s</a>', $link, $link);

        $this->mailer->raw($message, function (Message $m) use ($user) {
            $m->to($user->email)->subject('Activation mail');
        });


    }

    public function activateUser($token)
    {
        $activation = $this->activationRepo->getActivationByToken($token);

        if ($activation === null) {
            return null;
        }

        $user = User::find($activation->user_id);

        $user->activated = true;

        $user->save();

        $this->activationRepo->deleteActivation($token);

        return $user;

    }

    private function shouldSend($user)
    {
        $activation = $this->activationRepo->getActivation($user);
        return $activation === null || strtotime($activation->created_at) + 60 * 60 * $this->resendAfter < time();
    }

}

AuthController

Override register method in AuthController inherited from RegistersUsers trait. We don't want to immediately log in user but to send them an email instead and keep them logged out. Also we will inject ActivationService through constructor.

In AuthController, use classes:

use Illuminate\Http\Request;
use App\ActivationService;

Add protected variable:

protected $activationService;

And override following methods:

public function __construct(ActivationService $activationService)
{
    $this->middleware($this->guestMiddleware(), ['except' => 'logout']);
    $this->activationService = $activationService;
}

public function register(Request $request)
{
    $validator = $this->validator($request->all());

    if ($validator->fails()) {
        $this->throwValidationException(
            $request, $validator
        );
    }

    $user = $this->create($request->all());

    $this->activationService->sendActivationMail($user);

    return redirect('/login')->with('status', 'We sent you an activation code. Check your email.');
}

Activation Code Sent

To show status message add following in login.blade.php.

@if (session('status'))
    <div class="alert alert-success">
        {{ session('status') }}
    </div>
@endif
@if (session('warning'))
    <div class="alert alert-warning">
        {{ session('warning') }}
    </div>
@endif

If method authenticated exists in the AuthController it will be called from Laravel trait AuthenticatesUser. Use this method to block the user if it is not activated yet and, if required, resend the email.

public function authenticated(Request $request, $user)
{
    if (!$user->activated) {
        $this->activationService->sendActivationMail($user);
        auth()->logout();
        return back()->with('warning', 'You need to confirm your account. We have sent you an activation code, please check your email.');
    }
    return redirect()->intended($this->redirectPath());
}

Confirmation Warning

Here, if the user is not activated we will send the activation email again and show the error.

Final method will activate user:

public function activateUser($token)
{
    if ($user = $this->activationService->activateUser($token)) {
        auth()->login($user);
        return redirect($this->redirectPath());
    }
    abort(404);
}

Reset password functionality is not a problem since the user needs to check email to get the link. If he do so, he confirmed the email.

Author

I plan to write more articles about common laravel components. If you are interested let’s stay in touch.
comments powered by Disqus