Laravel comes with great Auth out-of-the-box. But some projects don't allow public registration, they send invitation links instead. This article will show you how to implement it.
![](/uploads/2019/01/laravel-invitation-request-1024x303.png)
Step 1. Invitations: DB Table and Model
To store our invitations, we will have a separate DB table invitations, with three fields: email, invitation_token (will be generated randomly) and registered_at (when invitation actually converted to user).
Schema::create('invitations', function (Blueprint $table) {
$table->increments('id');
$table->string('email')->unique();
$table->string('invitation_token', 32)->unique()->nullable();
$table->timestamp('registered_at')->nullable();
$table->timestamps();
});
And here's the app/Invitation.php model:
class Invitation extends Model
{
protected $fillable = [
'email', 'invitation_token', 'registered_at',
];
}
Step 2. Requesting Invitation
There are various ways to implement this, but we will assume that users can request an invite, by entering their email.
![](/uploads/2019/01/laravel-invitation-request-1024x303.png)
We need one route in routes/web.php file:
Route::get('register/request', 'Auth\RegisterController@requestInvitation')->name('requestInvitation');
Change in app/Http/Controllers/Auth/RegisterController.php are minimal - one simple method to show the form:
public function requestInvitation() {
return view('auth.request');
}
Blade file resources/views/auth/request.blade.php is really similar to login.blade.php, just different POST action for the form.
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row">
<div class="col-md-8 col-md-offset-2">
@if (session('error'))
<div class="alert alert-danger">
<p>{{ session('error') }}</p>
</div>
@endif
@if (session('success'))
<div class="alert alert-success">
<p>{{ session('success') }}</p>
</div>
@endif
<div class="panel panel-default">
<div class="panel-heading">Requesting Invitation</div>
<div class="panel-body">
<p>{{ config('app.name') }} is a closed community. You must have an invitation link to register. You can request your link below.</p>
<form class="form-horizontal" method="POST" action="{{ route('storeInvitation') }}">
{{ csrf_field() }}
<div class="form-group{{ $errors->has('email') ? ' has-error' : '' }}">
<label for="email" class="col-md-4 control-label">E-Mail Address</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control" name="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email'))
<span class="help-block">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</div>
</div>
<div class="form-group">
<div class="col-md-8 col-md-offset-4">
<button type="submit" class="btn btn-primary">
Request An Invitation
</button>
<a class="btn btn-link" href="{{ route('login') }}">
Already Have An Account?
</a>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
The main part is probably POST to this:
<form class="form-horizontal" method="POST" action="{{ route('storeInvitation') }}">
Let's create this route by adding another line into routes/web.php file:
Route::post('invitations', 'InvitationsController@store')->middleware('guest')->name('storeInvitation');
Next, we create the Controller mentioned in the route. app/Http/Controllers/InvitationsController will look like this:
public function store(StoreInvitationRequest $request)
{
$invitation = new Invitation($request->all());
$invitation->generateInvitationToken();
$invitation->save();
return redirect()->route('requestInvitation')
->with('success', 'Invitation to register successfully requested. Please wait for registration link.');
}
There are a few "hidden" things here. First, Form Request file for validation - app/Http/Requests/StoreInvitationRequest.php with these methods:
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'email' => 'required|email|unique:invitations'
];
}
/**
* Custom error messages.
*
* @return array
*/
public function messages()
{
return [
'email.unique' => 'Invitation with this email address already requested.'
];
}
Basically, we're restricting invitations to one only per unique email.
Finally, we need to generate random invitation token, and for that you can implement any logic you want, I've chosen a method in the model app/Invitation.php:
public function generateInvitationToken() {
$this->invitation_token = substr(md5(rand(0, 9) . $this->email . time()), 0, 32);
}
Notice: Yes, the encoding algorithm is pretty random, you can use something like UUID or anything you prefer.
I will remind you how new invitation is stored from Controller:
$invitation = new Invitation($request->all());
$invitation->generateInvitationToken();
$invitation->save();
Step 3. Viewing Invitations from Administrator
This one is pretty simple - we just need to view all the invitations in a table, and administrator will copy-paste the links and send them wherever they like - via email or Slack etc.
![](/uploads/2019/01/laravel-invitation-links-1024x281.png)
/**
* Invitations group with auth middleware.
* Even though we only have one route currently, the route group is for future updates.
*/
Route::group([
'middleware' => ['auth', 'admin'],
'prefix' => 'invitations'
], function() {
Route::get('/', 'InvitationsController@index')->name('showInvitations');
});
So, URL /invitations will show the table we want. Here we use a new Middleware we called 'admin'. Here's app/Http/Middleware/IsAdmin.php:
class IsAdmin
{
public function handle($request, Closure $next)
{
if (!auth()->check() || auth()->user()->email != 'admin@admin.com') {
return redirect(route('home'));
}
return $next($request);
}
}
Notice: For simplicity, we're just checking user's email to be admin@admin.com, but in real projects you probably want to implement roles/permissions in Laravel or with a package like Spatie Laravel Permissions.
We also need to register this middleware in app/Http/Kernel.php - see last line in this array, we assign a name 'admin' which we used earlier in routes:
protected $routeMiddleware = [
'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'admin' => \App\Http\Middleware\IsAdmin::class,
];
Now, let's get to viewing our invitations table. It's pretty simple, here's app/Http/Controllers/InvitationsController.php:
public function index()
{
$invitations = Invitation::where('registered_at', null)->orderBy('created_at', 'desc')->get();
return view('invitations.index', compact('invitations'));
}
And resources/views/invitations/index.blade.php:
@extends('layouts.app')
@section('content')
<div class="container">
<div class="page-header" style="margin-top: 0">
<h1>Invitation Requests</h1>
</div>
<div class="panel panel-default" style="margin-top: 20px">
<div class="panel-heading">Pending Requests <span class="badge">{{ count($invitations) }}</span></div>
<div class="panel-body" style="padding: 0;">
@if (!empty($invitations))
<table class="table table-responsive table-striped" style="margin-bottom: 0">
<thead>
<tr>
<th>Email</th>
<th>Created At</th>
<th>Invitation Link</th>
</tr>
</thead>
<tbody>
@foreach ($invitations as $invitation)
<tr>
<td><a href="mailto:{{ $invitation->email }}">{{ $invitation->email }}</a></td>
<td>{{ $invitation->created_at }}</td>
<td>
<kbd>{{ $invitation->getLink() }}</kbd>
</td>
</tr>
@endforeach
</tbody>
</table>
@else
<p>No invitation requests!</p>
@endif
</div>
</div>
</div>
@endsection
The main thing here is to show the invitation link:
<kbd>{{ $invitation->getLink() }}</kbd>
What is getLink()? It's a method inside app/Invitation.php:
public function getLink() {
return urldecode(route('register') . '?invitation_token=' . $this->invitation_token);
}
In other words, we will use the same /register URL but will accept it only with ?invitation_token parameter. This is our next step.
Step 4. Processing Invitation Links
Now, let's say someone has got an invitation link: yourdomain.com/register?invitation_token=XXXXXXXXXXXX. How to process it?
First, we will override the app/Http/Controllers/Auth/RegisterController.php and method showRegistrationForm():
public function showRegistrationForm(Request $request)
{
$invitation_token = $request->get('invitation_token');
$invitation = Invitation::where('invitation_token', $invitation_token)->firstOrFail();
$email = $invitation->email;
return view('auth.register', compact('email'));
}
Next, in registration form we will disable email field from changing and instead add it as input hidden field - in resources/views/auth/register.blade.php:
<input type="email" class="form-control disabled" value="{{ $email }}" disabled>
<input id="email" type="hidden" class="form-control disabled" name="email" value="{{ $email }}">
![](/uploads/2019/01/laravel-invitation-register-1024x560.png)
Finally, we need to restrict this URL from being accessed without invitation_token parameter. For this, we create another middleware, in app/Http/Middleware/HasInvitation.php:
namespace App\Http\Middleware;
use App\Invitation;
use Closure;
use Illuminate\Database\Eloquent\ModelNotFoundException;
class HasInvitation
{
public function handle($request, Closure $next)
{
/**
* Only for GET requests. Otherwise, this middleware will block our registration.
*/
if ($request->isMethod('get')) {
/**
* No token = Goodbye.
*/
if (!$request->has('invitation_token')) {
return redirect(route('requestInvitation'));
}
$invitation_token = $request->get('invitation_token');
/**
* Lets try to find invitation by its token.
* If failed -> return to request page with error.
*/
try {
$invitation = Invitation::where('invitation_token', $invitation_token)->firstOrFail();
} catch (ModelNotFoundException $e) {
return redirect(route('requestInvitation'))
->with('error', 'Wrong invitation token! Please check your URL.');
}
/**
* Let's check if users already registered.
* If yes -> redirect to login with error.
*/
if (!is_null($invitation->registered_at)) {
return redirect(route('login'))->with('error', 'The invitation link has already been used.');
}
}
return $next($request);
}
}
There are a few more things we check there - whether the link was already used, or whether invitation_token is invalid.
Finally, in our routes/web.php we override default registration route with this one:
Route::get('register', 'Auth\RegisterController@showRegistrationForm')
->name('register')
->middleware('hasInvitation');
Aaaaaand, that's it. With these steps, you have an invite-only registration system.
Repository for this project is available here: https://github.com/LaravelDaily/Laravel-Auth-Invitations
No comments or questions yet...