Laravel User Registration with Email Activation

This post documents how to add an e-mail confirmation to the Laravel User registration that is generated by the Artisan console. In this example, the registered user will not be able to login until the activation link that is sent to the users registered e-mail account has been loaded into the browser.

Requirements

An instance of Laravel 5.2 setup for local development with authentication scaffolding is required. Refer to the first page of my recent post on Laravel User Authentication with Ajax Validation for more information on installing Laravel 5.2 using Composer and generating the authentication scaffolding with artisan.

User Model

I like placing model classes in their own directory. This can be done with just a few minor changes after moving the User class into a new app/Models folder.

mkdir app/Models

mv app/User.php app/Models

Edit app/Models/User.php, update the App namespace to App\Models.

User.php
<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    ...

Throughout this tutorial you will encounter an ellipsis … in the code examples. These are not a part of the code and are there only to denote code that is being skipped and not applicable to the example. To view the entire file, checkout the email-activation branch in the source code.

Edit app/Http/Controllers/Auth/AuthController.php, update use App\User to use App\Models\User.

AuthController.php
<?php

namespace App\Http\Controllers\Auth;

use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

class AuthController extends Controller
{
    ...

Edit, config/auth.php, update the authentication drivers user provider.

'users' => [
    'driver' => 'eloquent',
    'model' => App\User::class,
],

REPLACE

'model' => App\User::class,

WITH

'model' => App\Models\User::class,

Database Migration Files

Generate a database migration file for creating the user_activations table. This table stores the activation codes until they are used.

create_user_activations_table
php artisan make:migration create_user_activations_table --create=user_activations

Edit the migration file up function to create these three fields in the user_activations table.

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

Generate a database migration file for adding a column to the users table.

php artisan make:migration alter_users_table_add_activated_col

Edit the migration file up function to add the activated field to the users table.

public function up()
{
    Schema::table('users', function($table) {
        $table->boolean('activated')->default(false);
    });
}

Run the migration

php artisan migrate

ActivationRepository Class

Create a Repositories folder in the app directory

mkdir app/Repositories

Create the ActivationRepository class file in app/Repositories. This class contains functions for generating activation tokens and updating the tables in the database.

ActivationRepository.php
<?php

namespace App\Repositories;

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

class ActivationRepository
{
    protected $db;
    protected $table = 'user_activations';

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

    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();
    }

    public function createActivation($user)
    {
        $activation = $this->getActivation($user);

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

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

    private function regenerateToken($user)
    {
        $token = $this->getToken();
        $this->db->table($this->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;
    }
}

ActivationFactory Class

Create a Factories folder in the app directory

mkdir app/Factories

Create the ActivationFactory class file in app/Factories.

ActivationFactory.php
<?php

namespace App\Factories;

use App\Models\User;
use App\Repositories\ActivationRepository;
use Illuminate\Mail\Mailer;
use Illuminate\Mail\Message;

class ActivationFactory
{
    protected $activationRepo;
    protected $mailer;
    protected $resendAfter = 24;

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

    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 %s', $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();
    }
}

Route

Create a route for the e-mail activation link in app/Http/routes.php. This user.activate route should be added after Route::auth()

routes.php
<?php
...
Route::auth();

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

AuthController

Make the following updates to app/Http/Controllers/Auth/AuthController.php

REPLACE

use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;

WITH

use App\Factories\ActivationFactory;
use App\Models\User;
use Validator;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ThrottlesLogins;
use Illuminate\Foundation\Auth\AuthenticatesAndRegistersUsers;
use Illuminate\Http\Request;

REPLACE

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

WITH

protected $activationFactory;

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

At the bottom of the controller, add these three functions after the create function. The register function we are adding will override the register method inherited from the AuthenticatesAndRegistersUsers trait.

...

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

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

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

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

    return redirect('/login')->with('activationStatus', true);
}

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

public function authenticated(Request $request, $user)
{
    if (!$user->activated) {
        $this->activationFactory->sendActivationMail($user);
        auth()->logout();
        return back()->with('activationWarning', true);
    }
    return redirect()->intended($this->redirectPath());
}

Authentication Language Lines

For the activation status messages that we need to display to the user, update the resources/lang/en/auth.php file.

en/auth.php
<?php

return [

...

    'activationStatus' => 'We sent you an activation code. Please check your e-mail.',
    'activationWarning' => 'You need to confirm your account. We have sent you an activation code, please check your e-mail.',

...

];

Add the following inside the form at resources/views/auth/login.blade.php Blade view to render the activation messages.

login.blade.php
...

@if (session('activationStatus'))
    <div class="alert alert-success">
        {{ trans('auth.activationStatus') }}
    </div>
@endif

@if (session('activationWarning'))
    <div class="alert alert-warning">
        {{ trans('auth.activationWarning') }}
    </div>
@endif

Source Code
comments powered by Disqus