Invite Only Chat with Laravel and Vue

9 min read
Chimezie E.
Chimezie E.
Published April 28, 2020

Group chat can get a bit messy, especially if it’s public and anyone can join. Making a group chat invite-only can help manage the content of the group and the sanity of those in it, in addition to preventing spam users and bots.

In this tutorial, we’ll be building an invite-only chat room where only admin users can invite other users to join a chat room. Upon joining the chat room, users will be able to chat with other users in realtime.

The complete code for this tutorial is available on GitHub.

Prerequisites

To complete this tutorial, you'll need the following tech:

To follow along with this tutorial, a basic understanding of Laravel, Vue.js, and JavaScript would also be helpful.

Getting Our Stream Chat Keys

To use Stream Chat in our app, we need to grab our Stream Chat API keys. Stream Chat allows you to build a fully-functional chat app in hours, instead of weeks, or even months; we'll use it to build our simple example app in minutes! To get started log in to Stream or create an account. Upon successfully entering your credentials, you'll be delivered to your dashboard, where you can create a new app by clicking the Create App button:

Screenshot of the Modal to Create a New Stream Chat App

Once that is done, you should see an API KEY and SECRET for your newly created application:

Screenshot of the Stream Chat Dashboard

Take note of these keys as we’ll be needing them shortly.

Getting Started

We’ll be using the Laravel installer to create a new Laravel application. Run the following command to create a new Laravel application with authentication scaffolding:

$ laravel new stream-invite-only-group-chat --auth

Once that’s done, let’s install the npm dependencies:

$ cd stream-invite-only-group-chat
$ yarn install

Now, we can begin building out our application!

Building the Application

We’ll start by updating the users table migration in our create_users_table.php file, within database/migrations, as shown below:

Schema::create('users', function (Blueprint $table) {
  $table->id();
  $table->string('username');
  $table->string('email')->unique();
  $table->boolean('is_admin')->default(false);
  $table->timestamp('email_verified_at')->nullable();
  $table->string('password');
  $table->rememberToken();
  $table->timestamps();
});

We updated the name field to username and added a new field called is_admin, which will be used to determine if a user is an admin or not.

Let’s update the fillable fields in our User.php file, in the app directory, to have the username field instead, as well:

protected $fillable = [
  'username', 'email', 'password',
];

Next, we’ll create an Invite model:

$ php artisan make:model Invite -mc

In addition to creating an Invite model, the command above will create a migration, as well as a controller for the model.

Open up the created migration and update the up method in the create_invites_table.php file, within database/migrations, as shown below:

Schema::create('invites', function (Blueprint $table) {
  $table->id();
  $table->string('email')->unique();
  $table->string('token');
  $table->boolean('accepted')->default(false);
  $table->timestamps();
});

The fields are pretty straightforward; the accepted field will be used to determine if an invite has been accepted or not.

Also, let’s mark the fields as fillable in Invite.php, in the app directory:

protected $fillable = [
  'email', 'token'
];

Before we run the migration, let’s set up our database. For the purpose of this tutorial, we’ll be using SQLite. Update DB_CONNECTION inside .env as below:

APP_NAME=Chatroom
DB_CONNECTION=sqlite

Because we are making use of SQLite, we can get rid of the other DB environment variables.

By default, Laravel will look for a database.sqlite file, so let’s create it:

$ touch database.sqlite

Now, we can run the migration:

$ php artisan migrate

Before we move on to adding functionality to our application, let’s add our Stream keys to our .env file:

MIX_STREAM_API_KEY=YOUR_STREAM_API_KEY
MIX_STREAM_API_SECRET=YOUR_STREAM_API_SECRET

Be sure to update "YOUR_STREAM_API_KEY" and "YOUR_STREAM_API_SECRET" to the values you grabbed from your Stream Chat dashboard.

Creating Channel and Admin Users

Now, let’s start building the group chat functionality. We’ll start by creating a new channel, which we’ll call "Chatroom", and an admin user, to administer the group. We’ll be creating a console command to handle these. Run the command below to create a new console command:

$ php artisan make:command InitializeChannel

This will create a new InitializeChannel.php file inside app/Console/Commands.

Since we’ll be making use of the Stream Chat PHP SDK, let’s install it:

$ composer require get-stream/stream-chat

With that installed, open up InitializeChannel.php, located at app/Console/Commands, and replace the contents with the following:

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use GetStream\StreamChat\Client as StreamClient;
use App\User;
use Illuminate\Support\Facades\Hash;

class InitializeChannel extends Command
{
    protected $signature = 'channel:init';
    
    protected $description = 'Initialize channel with admin user';
    
    public function __construct()
    {
        parent::__construct();
    }
    
  public function handle()
  {
    $user = new User;

    $user->username = 'mezie';
    $user->email = 'chimezie@adonismastery.com';
    $user->is_admin = true;
    $user->password = Hash::make('password');

    $user->save();

    $client = new StreamClient(
      env('MIX_STREAM_API_KEY'),
      env('MIX_STREAM_API_SECRET'),
      null,
      null,
      9
    );

    $client->updateUser([
      'id' => $user->username,
      'name' => $user->username,
      'role' => 'admin'
    ]);

    $channel = $client->Channel('messaging', 'Chatroom', [
      'created_by' => $user->username,
    ]);

    $channel->addMembers([$user->username]);

    return $this->info('Channel initialized');
  }
}

We define the signature of the command and give it a description. Inside the handle method, we create an admin user, then we create the user on Stream as well, assigning the user an admin role. Then, we create the channel and make the newly created user the creator of the channel. Next, we add the user as a member of the channel. Lastly, we return a success message.

Now, we can run the channel:init command:

$ php artisan channel:init

Since we want users to join our group chat only by invitation, let’s remove the ability to register, which was added by Laravel authentication scaffolding. We can do that by updating web.php, in the routes directory, with the below:

Auth::routes(['register' => false]);

Inviting Users

With the channel and admin users created, let’s add the functionality to invite users to our group chat. We’ll start by creating the routes. Add the code below inside web.php:

Route::get('/invites/create', 'InviteController@create');
Route::post('/invites', 'InviteController@store');

Since we already have InviteController.php, let’s open it up and add the following code in it:

public function create()
{
  return view('invites.create');
}

Next, let’s create the view file. Inside the resources/views directory, create a new invites directory and create a create.blade.php file inside it, then paste the following code in it:

@extends('layouts.app')

@section('content')
<div class="container">
  <div class="row justify-content-center">
    <div class="col-md-8">
      <div class="card">
        <div class="card-header">Invite User</div>

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

          <form method="POST" action="/invites">
            @csrf

            <div class="form-group">
              <label for="email">E-Mail Address</label>
              <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required>

              @error('email')
                <span class="invalid-feedback" role="alert">
                  <strong>{{ $message }}</strong>
                </span>
              @enderror
            </div>
            <div class="form-group">
              <button type="submit" class="btn btn-primary">Send Invite</button>
            </div>
          </form>
        </div>
      </div>
    </div>
  </div>
</div>
@endsection

The form has just one field, which is for capturing the email address to send an invitation to. Above the form, we have in place to display flash messages, if there are any:

Screenshot of the Invite User Page
Building your own app? Get early access to our Livestream or Video Calling API and launch in days!

Let’s move on to adding the implementation for processing the form. Add the following code to the top of InviteController.php:

use App\Invite;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;

public function store(Request $request)
{
  $invite = Invite::firstWhere('email', $request->email);

  if (!is_null($invite)) {
    if ($invite->accepted) {
      return back()->with('status', 'User is already a member');
    } else {
      return back()->with('status', 'An invite has been sent to user already');
    }
  }

  $invite = Invite::create([
      'email' => $request->email,
      'token' => Str::random(40),
  ]);

  $mailBody = 'You\'ve been invited to join Chatroom: ' . url('invites', [$invite->token]);

  Mail::raw($mailBody, function ($message) use ($invite) {
    $message->to($invite->email);
    $message->from('chimezie@adonismastery.com');
    $message->subject('Join Chatroom');
  });

  return back()->with('status', 'Invite sent!');
}

First, we get an invite matching the submitted email address. If an invite already exists for the email address, we flash an appropriate message depending on whether the invite as been accepted or not. If no invite was found, we create one for the email address, then send an email with the invitation link.

For this tutorial, we won’t be setting up a mail driver. Instead, we’ll log the mail to a file. Let’s set the MAIL_MAILER to "log" inside .env as shown below:

$ MAIL_MAILER=log

Now, if we try sending an invite, it should be logged inside storage/logs/laravel.log and look something like the below:

[2020-04-25 13:53:21] local.DEBUG: Message-ID: <f38df02fbef7a05d3b21f9d8f4f77210@swift.generated>
Date: Sat, 25 Apr 2020 13:53:21 +0000
Subject: Join Chatroom
From: chimezie@adonismastery.com
To: johndoe@example.com
MIME-Version: 1.0
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

You've been invited to join Chatroom: http://stream-invite-only-group-chat.test/invites/pgl0wYpuf4AdUGdM1sVa0JaRSZQBGS9PaP2eEHs4 

I’m using Laravel Valet, hence my URL: http://stream-invite-only-group-chat.test

Let’s add a link to access the invite users page. We’ll add it in the navbar (app.blade.php) immediately after the @else:

@if (Auth::user()->is_admin)
  <li class="nav-item">
    <a class="nav-link" href="/invites/create">Invite</a>
  </li>
@endif

While we are here, let’s update the navbar to show the user’s username instead of the name:

{{ Auth::user()->username }}

With these changes, your navbar should look like the below:

Screenshot of the Navbar with Link to Invite Page and User's Username

Once a user is sent an invitation, the user can click on the link in the email to join the group chat. To make this possible, add the following code inside web.php:

Route::get('/invites/{token}', 'InviteController@show');
Route::post('/register', 'Auth\RegisterController@register');

Next, add the following code inside InviteController.php:

public function show($token)
{
  $invite = Invite::where('token', $token)->firstOrFail();

  if ($invite->accepted) {
    return redirect()->route('home');
  }

  return view('auth.register', compact('invite'));
}

First, we retrieve the invite matching the token, if it exists. If the invitation has been accepted, we simply redirect the user to the home route. Otherwise, we render the "register" view and pass along the invite.

The register view is already in place; we just need to update it slightly. Replace the name and email fields in resources/views/auth/register.blade.php as below:

<div class="form-group row">
  <label for="username" class="col-md-4 col-form-label text-md-right">{{ __('Username') }}</label>
  <div class="col-md-6">
    <input id="username" type="text" class="form-control @error('username') is-invalid @enderror" name="username" value="{{ old('username') }}" required autocomplete="username" autofocus>

    @error('username')
      <span class="invalid-feedback" role="alert">
        <strong>{{ $message }}</strong>
      </span>
    @enderror
  </div>
</div>

<div class="form-group row">
  <label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
  <div class="col-md-6">
    <input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $invite->email }}" required readonly>
    @error('email')
      <span class="invalid-feedback" role="alert">
        <strong>{{ $message }}</strong>
      </span>
    @enderror
  </div>
</div>

We made the email field read-only and set the value to the email address on the invite.

Screenshot of the Register Page with Preset Email

We’ll still make use of the default Laravel RegisterController, at app/Http/Controllers/Auth/, to handle registration. Open it up and update validator and create methods to make use of username instead of name like below:

protected function validator(array $data)
{
  return Validator::make($data, [
    'username' => ['required', 'string', 'max:255'],
    'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
    'password' => ['required', 'string', 'min:8', 'confirmed'],
  ]);
}

protected function create(array $data)
{
  return User::create([
    'username' => $data['username'],
    'email' => $data['email'],
    'password' => Hash::make($data['password']),
  ]);
}

Still inside RegisterController.php, add the following code to the very top:

use GetStream\StreamChat\Client as StreamClient;

protected function registered($user)
{
  $invite = Invite::firstWhere('email', $user->email);

  $invite->accepted = true;
  $invite->save();

  $client = new StreamClient(
    env('MIX_STREAM_API_KEY'),
    env('MIX_STREAM_API_SECRET'),
    null,
    null,
    9
  );

  $client->updateUser([
    'id' => $user->username,
    'name' => $user->username,
  ]);

  $channel = $client->Channel('messaging', 'Chatroom');
  $channel->addMembers([$user->username]);
}

Once a user registers successfully, the registered method is called. Inside this method is the perfect place to mark the invite as accepted, create the user on Stream, and, finally, add the user to the "Chatroom" channel.

Generating an Authentication Token

Let’s add the functionality for generating an authentication token, which Stream Chat will use to authenticate a user.

First, let’s create the endpoint inside routes/api.php:

Route::post('/tokens', 'TokenController@generate');

Next, create a TokenController:

$ php artisan make:controller TokenController

and add the following to it:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use GetStream\StreamChat\Client as StreamClient;

class TokenController extends Controller
{
  public function generate(Request $request)
  {
    $client = new StreamClient(
      env('MIX_STREAM_API_KEY'),
      env('MIX_STREAM_API_SECRET'),
      null,
      null,
      9
    );

    return response()->json([
      'token' => $client->createToken($request->username)
    ]);
  }
}

This generates a token using the user’s username as the ID.

Adding Chat Functionality

With the backend functionality in place, let’s move on to adding the chat functionality. We are going to rename ExampleComponent.vue, inside the resource/js directory, to "ChatRoom.vue" and update its reference inside resources/js/app.js as below:

Vue.component('chat-room', require('./components/ChatRoom.vue').default);

Now, replace the content of the ChatRoom component with the following:

<template>
  <div class="container">
    <div class="row">
      <div class="col-md-3">
        <div class="card">
          <div class="card-header">Members</div>
          <div class="card-body">
            <ul class="list-group list-group-flush">
              <li
                class="list-group-item"
                v-for="(member, id) in members"
                :key="id"
              >{{ member.user.name }}</li>
            </ul>
          </div>
        </div>
      </div>
      <div class="col-md-9">
        <div class="card">
          <div class="card-header">Chats</div>
          <div class="card-body">
            <dl v-for="message in messages" v-bind:key="message.id">
              <dt
                :class="{  'text-right': message.user.id === authUser.username }"
              >{{ message.user.name }}</dt>
              <dd
                :class="{  'text-right': message.user.id === authUser.username }"
              >{{ message.text }}</dd>
            </dl>
            <hr />
            <span class="help-block" v-if="status" v-text="status" style="font-style: italic;"></span>
            <form @submit.prevent="sendMessage" method="post">
              <div class="input-group">
                <input
                  type="text"
                  v-model="newMessage"
                  class="form-control"
                  placeholder="Type your message..."
                />
                <div class="input-group-append">
                  <button class="btn btn-primary">Send</button>
                </div>
              </div>
            </form>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import axios from "axios";
import { StreamChat } from "stream-chat";
export default {
  name: "ChatRoom",
  props: {
    authUser: {
      type: Object,
      required: true
    }
  },
  data() {
    return {
      token: null,
      channel: null,
      client: null,
      members: [],
      messages: [],
      newMessage: "",
      status: ""
    };
  },
  async created() {
    await this.getToken();
    await this.initializeStream();
    await this.initializeChannel();
  },
  methods: {
    async getToken() {
      const { data } = await axios.post("/api/tokens", {
        username: this.authUser.username
      });
      this.token = data.token;
    },
    async initializeStream() {
      this.client = new StreamChat(process.env.MIX_STREAM_API_KEY, {
        timeout: 9000
      });
      await this.client.setUser(
        { id: this.authUser.username, name: this.authUser.username },
        this.token
      );
    },
    async initializeChannel() {
      this.channel = this.client.channel("messaging", "Chatroom");
      const { members, messages } = await this.channel.watch();
      this.members = members;
      this.messages = messages;
      this.channel.on("message.new", event => {
        this.messages.push({
          text: event.message.text,
          user: event.message.user
        });
      });
      this.channel.on("member.added", event => {
        this.members.push(event);
        this.status = `${event.user.name} joined the chat`;
      });
    },
    async sendMessage() {
      await this.channel.sendMessage({
        text: this.newMessage
      });
      this.newMessage = "";
    }
  }
};
</script>

The template is made up of two parts: the members list and the chat itself. We generate a token for the authenticated user, then we use the token to instantiate the Stream Chat client. We also initialize the channel and watch for activities on the channel, which allows us to know when a new message is sent on the channel as well when new users join the channel. Lastly, we have the functionality for sending a new message.

For a guide that covers these in detail, check out my previous tutorial.

Now, let’s install the Stream Chat JavaScript SDK:

$ yarn add stream-chat

Next, let’s make use of the ChatRoom component. Update home.blade.php as shown below:

@extends('layouts.app')

@section('content')
    <chat-room :auth-user="{{ auth()->user() }}"></chat-room>
@endsection

The ChatRoom component accepts the authenticated user as props, which we are passing in.

Screenshot of the Chatroom Page

Securing Our Application

Remember we said only an admin user can invite users to join our chatroom? But, as it stands, anybody can do this, even if they're not logged in. Let’s change that! Laravel provides an Authenticate middleware, by default, which we can use to enforce access to only authenticated users. We’ll create another middleware for enforcing an authenticated user is an admin.

Run the command below to create an Admin middleware:

$ php artisan make:middleware Admin

and update the handle method as shown below:

public function handle($request, Closure $next)
{
  if (!auth()->user()->is_admin) {
    return redirect()->route('home');
  }

  return $next($request);
}

Here, we are simply redirecting the authenticated user to the home route if the user is not an admin. If they are an admin, we allow the user to access the requested page.

Before we can make use of the middleware, we need to register it. We’ll register it as a route middleware by adding it inside the $routeMiddleware array, in app/Http/Kernel.php, like so:

protected $routeMiddleware = [
  'admin' => \App\Http\Middleware\Admin::class,
];

Now, we can make use of the middleware inside web.php using the admin key/identifier:

Route::get('/invites/create', 'InviteController@create')->middleware(['auth', 'admin']);
Route::post('/invites', 'InviteController@store')->middleware(['auth', 'admin']);

Now, not only does a user have to be authenticated to access these routes, but they must also be an admin!

Wrapping Up

In this tutorial, we have seen how to build an invite-only group chat using Laravel, Vue.js, and Stream Chat. We created multiple pages and authentication layers to our simple chat, but there is so much more you can do! To learn more about Stream Chat and its exciting features, check out the docs!

Thanks for reading, and happy coding!

Integrating Video With Your App?
We've built a Video and Audio solution just for you. Check out our APIs and SDKs.
Learn more ->