Inviting Users with devise_invitable

Published Jan 29, 2017Last updated Feb 22, 2017
Inviting Users with devise_invitable

Today we're going to be talking about devise_invitable and using it to add it to your application to invite users.

If you're building an application, chances are the application will most likely have some kind of collaborative use for it. Thus the use for an invitation system. There are a few things you'll need to handle with an invitation system. When you sign up you have to validate they have a password, but if you invite them we need to create a user record without a password and we send them an email with a token which may or may not expire. Also then we'll need to actually create the association for the user that's just been created/invited. So there's a fair number of gotchas to keep in mind.

Using devise_invitable means that it will handle most of this for you, allowing you to focus on the core features for your app.

We're going to accomplish this by building a Rails app with Users and Projects, but allow Users to invite other users to projects. So let's dive into it.

We're going to create a new Rails app in the Terminal now.

Quick note: If you have multiple rails versions installed you can specify a version by using it like so rails 5.0.0.rc1 new project_manager.

Here we assume Rails 5 is what the system is using on its own, but if you have multiple versions installed you may need to specify it.

rails new project_manager

Now let's jump into the directory it created for us and then open the Gemfile.

cd project_manager

Gemfile

gem 'devise'

Now we need to run bundle install

Once devise has been installed we can run the generator for Devise

rails g devise:install

Then we'll generate the User model by using another Devise generator. This one generates our User model which we will use to sign up and login with.

rails g devise User

Then run the migrations

rake db:migrate

Now we will generate a scaffold for our Projects.

We could generate one with a user_id and a name, and use the user_id to associate it with the user that owns the project, but let's go one step further and we will use a ProjectUser association to do it instead because we will need to do that for users we invite anyways.

rails g scaffold Project name

This is what we will use to tell our app which users are on which projects. We'll use the role field to determine what level of access they should have as a user or an owner, and the owner will be able to do things like delete users or invite them to the Project.

rails g model ProjectUser project_id:integer user_id:integer role

When we create a project we will automatically add the user that created the project as an owner.

Now let's run our migrations again because we've generated a scaffold and another model.

rake db:migrate

We need to go set a root path so let's do that.

config/routes.rb

root to: "projects#index"

Now we'll boot up our rails server in the Terminal and then go checkout http://localhost:3000

You'll see you can now view the Projects scaffold it generated for us. Before we go any further we should jump back to the Projects controller and make sure that the users are logged in before they can do anything.

app/controllers/projects_controller.rb

before_action :authenticate_user!

We add that to the top of the controller and that forces us to login before it does anything.

So now you can sign up for an account. You could create a project right now but it won't have any way to know that your the one who created it so we need to jump to the User model and add the associations.

app/models/user.rb

has_many :project_users
has_many :projects, through: :project_users

Then we'll go to the Project model and do basically the same, only making sure to change the fact that the Project has_many users instead of Projects (because a Project can't have a project associated to it, only a user).

app/models/project.rb

has_many :project_users
has_many :users, through: :project_users

Now we should go to the Project User model and add the belongs to's.

app/models/project_user.rb

belongs_to :project
belongs_to :user

So that's the associations handled. But now we need to go modify the Projects controller to create the ProjectUser record for us when we create a Project.

app/controllers/projects_controller.rb

def create
  @project = Project.new(project_params)
  @project.project_users.new(user: current_user, role: "owner")
  # omitted rest of action below
end

Basically what we're doing here is making sure to create a new project_user record with our current user and adding a role of owner to it. This is a required step to ensure that we know who's the owner for the project. If you're adding these through the console you'll want to make sure to do the project_users creation as well.

You could do this in a callback but that can get messy quick. One way you could handle it is creating a service object to do this for you every time you create a project but for this app we'll just keep it simple and remember to do it every time we create a project, since we're just using the app to create the Projects.

If you use the app to create a Project now and check your rails server logs you can see that it inserted into the Projects table and there's another insert into the project_users table with our user_id and a role of owner.

Jumping back to our Projects Controller we need to make sure our index action only shows the current user's projects.

Because we have the has_many :projects, through: :project_users on our user model we now have access to use current_user.projects to get the list of projects the user has.

app/controllers/projects_controller.rb

def index
  @projects = current_user.projects
end

Then we want to make sure to view a project you're associated with the project as well so we'll change the set_project private method in the controller as well

private
  def set_project
    @project = current_user.projects.find(params[:id])
  end

Now we have our projects working we should create a second user on the app and then login as them. One handy way to do this is to use private browsing so that you can be logged in as two users at once.

Once you've done that and logged in you should not be able to see the project you created earlier. Now we can begin to install devise_invitable because we want the first user to be able to invite the second user to the Project.

When you're installing devise_invitable one thing to keep in mind is sometimes when changes are made in Devise you need to make sure you're using a compatible version of devise_invitable.

Gemfile

gem 'devise_invitable'

Then we run bundle install and then run the generators. rails g devise_invitable:install

If you're using 5.0.0.rc1 then chances are you just ran into an error if they haven't published a new version yet so let's use the gem from github instead and remember to run bundle install to grab that.

gem 'devise_invitable', github: 'scambra/devise_invitable'

Now we should be able to rails g devise_invitable:install and rails g devise_invitable User

This adds the fields and information needed for devise invitable to work, if you'd like more information on what it's doing you can check the wiki for the gem.

Now let's run the migrations rake db:migrate.

Remember to restart your rails server when you run bundle install!

Inviting Users

Rather than using the built in forms from the gem we'll build our own.

We'll create a form that accepts an email address, that looks up the email address submitted and then if the user exists adds them to the project, if not it will invite the user and add them to the project. Then we will be able to send out different emails based on if you were already a member or if you were not.

So we need to add routes for users underneath the projects, and we'll use these to invite the users. It's basically for the management of the users on the projects.

config/routes.rb

resources :projects do
  resources :project_users, path: :users, module: :projects
end

Now we have it scoped to projects as well. But we're using a path of users for the project_users resource.

You can view this by running rake routes and looking at the path before and after you add the path setting, by specifying users we are removing project from it which is basically redundant when you have projects/:project_id/users.

Next we need to create the folder and controller for the Projects::ProjectUsersController we just setup in the routes.

mkdir app/controllers/projects

touch app/controllers/projects/project_users_controller.rb

Now we can edit the app/controllers/projects/project_users_controller.rb empty file we just created.

class Projects::ProjectUsersController < ApplicationController
  def create

  end
end

We're going to be taking advantage of the Rails 5 attributes API that was implemented so we will go create an email attribute on the user.

app/models/user.rb

attribute :email, :string

If you're not using Rails 5 you can use an attr_accessor :email instead.

Let's jump to our project show page and create the form for inviting users.

app/views/projects/show.html.erb

...
<h4>Invite User</h4>
<%= form_for [@project, ProjectUser.new] do |f| %>
  <div>
    <%= f.text_field :email, required: true %>
  </div>
  <%= f.submit "Invite" %>
<%  end %>
...

We're setting the virtual attribute for the email then when you submit the form we'll look up the user based on the email so in the ProjectUser model we'll create a method to look up the user.

app/models/project_user.rb

def set_user_id
  existing_user = User.find_by(email: email)
  self.user = if existing_user.present?
                existing_user
              else
                User.invite!(email: email)
              end
end

We're looking up the user by their email address and if it exists then we set the user to the user record and if it doesn't then we shoot off an Invite using devise_invitable. All this does is assign the user inside of the model, but our controller action will take care of sending the emails.

Jumping over to the Projects::ProjectUsersController you'll see we've made a few changes to it

app/controllers/projects/project_users_controller.rb

class Projects::ProjectUsersController < ApplicationController
  before_action :authenticate_user!
  before_action :set_project

  def create

  end

  private
    def set_project
      @project = current_user.projects.find(params[:project_id])
    end

    def project_user_params
      params.require(:project_user).permit(:email)
    end
end

Here we've made sure the user is logged in and we're setting the project based off the project_id that is passed in through the routes. As well as we set up the params method to make sure there is an email.

Now we can make the create action actually do stuff.

def create
  project_user = @project.project_users.new(project_user_params)
  project_user.project = @project

  if project_user.save
    redirect_to @project, notice: 'Saved!'
  else
    redirect_to @project, alert: 'Failed saving!'
  end
end

But we need to go connect the set_user_id on the ProjectUser model. If you set an email we want to run this code, but if you didn't then we don't want to do anything. Using the new attributes API we can use it like it's a database attribute so we can do things like email? on the ProjectUser.

So let's go to the ProjectUser model and create a before_validation callback.

app/models/project_user.rb

before_validation :set_user_id, if: :email?

Then the code will run if you send an email on the ProjectUser.new form, it will then set the user_id for the project_users and the validations will pass and the record will be created on the project, thereby inviting the user.

We can now invite that second user we created earlier and you should get a Saved or Success message from the controller. But it would be great if we had some kind of what of displaying the users on the project show page.

app/views/projects/show.html.erb

...
  <h4>Users</h4>
  <% @project.users.each do |user| %>
    <div><%= user.email %></div>
  <% end %>
...

Now we can loop through each user and display their email address for each user on it.

If you invite a user that doesn't exist it will most likely error out because we haven't set the default_url_options when we installed Devise. So let's go to our environments file and add that to the development one.

config/environments/development.rb

# Set default host for mailers, remember to do this in production as well!
  config.action_mailer.default_url_options = { host: "localhost:3000" }

Be sure to restart your rails server after modifying the environments file.

Now the invitation will get sent and the user will be added to the project.

Refactoring

One thing to think about is that when you are first building the feature you'll most likely want to step through and add the logic for things like checking if the user exists, but as you finish you may be able to go back and refactor it to be less lines because the gems provide functionality to handle that themselves. For example the set_user_id method could be refactored like so:

app/models/project_user.rb

def set_user_id
  self.user = User.invite!(email: email)
end

Which is because devise_invitable actually handles most of the functionality we wrote ourselves in the gem.

So there you have it, adding users to projects or other things is relatively straightforward using devise_invitable.
If you want to send emails you can do those in the Projects::ProjectUsersController where it saves the project_user.

Credits for GoRails

This transcript was written by Andrew Fomera.

Discover and read more posts from Victor H
get started