Posted on • Originally published atrailsdesigner.com
Add Invite to Rails 8 Authentication
This article was originally published onRails Designer Build a SaaS
This is the third article on Building a SaaS with Ruby on Rails. This article continues where the previous one,adding sign up to Rails 8' authentication, stops.
Most SaaS products are used by multiple people from the same team or company. Although I would, when you start building your SaaS, urge you to keep it simple and just support one user on launch. I would álso urge you to build the blocks needed for teams in your app from the get-go. Having to deal with workspaces, teams and roles when you already have users is a bigger pain than you can imagine.
This article will focus on an important feature, and a metric you likely want to keep track off for new sign ups; invitations. Not just an invitation to use the app (though you can tweak it to do just that), but an invitation to the inviter's workspace.
As always the repo can be foundhere. Let's get right to it.
Adding workspaces
The previous article on adding sign ups, left the option to add a Workspace on sign up. Let's add that now. It's simple really. All it is, for now, is a record with ahas_many :users
. All business records your users create will belong to the workspace, giving the users access to them (but that will be for another article).
Let's create the model first:rails g model Workspace name:string
. Then update the existing User model by adding aworkpace_id:rails g migration add_workspace_id_to_users workspace_id:integer
.
Update the new Workspace model like this:
# app/models/workspace.rbclassWorkspace<ApplicationRecordhas_many:users,dependent: :destroyend
Then the User model:
classUser<ApplicationRecord# …belongs_to:workspace# …end
Let's also updateapp/models/current.rb for some nicety: adddelegate :workspace, to: :user
. This will allow you to doCurrent.workspace
to get the user's workspace. Nice!
Head over the Signup class and the actual code to create a new workspace on sign up:
classSignupincludeActiveModel::ModelincludeActiveModel::Attributesdefsave# …endprivatedefcreate_workspace_for(user)Workspace.create(name:"New Workspace").tapdo|workspace|workspace.users<<userendendend
Alright. Now the pieces are already in place to invite others to the Workspace.
Adding invites
What I am having in mind is the following:
- Workspace "owner" (the User would created the Workspace) adds email from invitee;
- Invitation model is created, with: email and inviter_id;
- After Invitation create, an email is sent to the email with an invite link;
- On clicking the link, invitee sees a form with email and password;
- Upon submit, an User model is created and attached to the Workspace.
Doesn't look all too bad when written out like this, right? Let's create the invitation model first:rails g model Invitation workspace_id:integer inviter_id:integer email_address:string accepted_at:datetime
.
Let's update the newly created model:
classInvitation<ApplicationRecordbelongs_to:workspacebelongs_to:inviter,class_name:"User"end
Let's also update the Workspace model by addinghas_many :invitations, dependent: :destroy
. This will allow to list all (pending) invitations per Workspace.
In its most basic form there needs to be two controller with two actions each:
- one for creating the invitation by the workspace owner (actions: new and create);
- one for accepting the invitation by the invitee (actions: new and create).
Let's do the invitations creations first. It is simple really!
classInvitationsController<ApplicationControllerdefindex@invitations=Current.workspace.invitationsenddefnew@invitation=Invitation.newenddefcreateifinvitation=Invitation.create(invitation_params)InvitationsMailer.invite(invitation).deliver_laterendredirect_toinvitations_pathendprivatedefinvitation_paramsparams.expect(invitation:[:email_address]).with_defaults(inviter_id:Current.user.id,workspace_id:Current.user.workspace_id)endend
Then add to the routesresources :invitations, only: %w[index new create]
. Now it's also clear what views are needed:
NB: as with all articles in this series, the UI is left up to you. Head over the articles fromRails Designer, thecomponents and check out the cool newForm Builder. It will give you beautiful form inputs by just typingform.input :email_address
. 👀
Simple list of invitations:
# app/views/invitations/index.html.erb<ul><%@invitations.eachdo|invitation|%><li><%=invitation.email_address%> -<%=invitation.accepted_at%></li><%end%></ul>
Then the form to create a new invitation:
<%=form_for@invitationdo|form|%><%=form.text_field:email_address%><%=form.submit%><%end%>
Simple enough! Let's create the mailer to send the invitation over:
# app/mailers/invitations_mailer.rbclassInvitationsMailer<ApplicationMailerdefinvite(invitation)@invitation=invitationmailsubject:"You are invited!",to:invitation.email_addressendend# app/views/invitations_mailer/invite.html.erb<p>Hereisyourinvitation.Clickthislinkto<%= link_to "get started", accept_invitation_url(token: @invitation.generate_token_for(:invitation)) %>.</p>
This shows a couple things that needs to be added and create the functionality for@invitation.generate_token_for(:invitation)
and the route, controller, class and views foraccept_invitation_url()
.
Let's do the easy one first, let's add the following to yourInvitation Active Model:
classInvitation<ApplicationRecord# …generates_token_for:invitation,expires_in:7.daysdoaccepted_atend# …end
This is a featureintroduced in Rails 7.1. This also gives the method find_by_token_for() to look up the invitation as you will see in a moment. The nice thing, it will returnnil
if theaccepted_at column is set.
Let's speed through the boilerplate for accepting invitations and then look at the class to add the new user to the workspace.
# app/controllers/accept_invitations_controller.rbclassAcceptInvitationsController<ApplicationControllerdefnew@invitation=Invitation.find_by_token_for(:invitation,params[:token])enddefcreateAcceptInvitation.create(invitations_params)redirect_toroot_pathendprivatedefinvitation_paramsparams.expect(accept_invitation:[:email_address,:password])endend# config/routes.rbRails.application.routes.drawdo# …resources:accept_invitations,only:%w[new create]end# app/views/accept_invitations/new.html.erb<%= form_with model: @invitation, url: accept_invitations_path do |form| %> <%=form.email_field:email_address%><%= form.password_field :password %> <%=form.submit%><% end %>
Wow! That was fast. 🏎️ Let's create theAcceptInvitation class. It will be similarform object is done previously forSignup.
classAcceptInvitationincludeActiveModel::ModelincludeActiveModel::Attributesattribute:email_address,:stringattribute:password,:stringattribute:token,:stringvalidates:email_address,presence:truevalidates:password,length:8..128defsaveifvalid?User.create!(email_address:email_address,password:password).tapdo|user|update_invitationadd_newuser,to:invitation.workspaceendendenddefmodel_nameActiveModel::Name.new(self,nil,self.class.name)endprivatedefupdate_invitationinvitation.update(accepted_at:Time.current)enddefadd_new(user,to:)to.users<<userenddefinvitation=Invitation.find_by_token_for(:invitation,token)end
This class is the place to handle everything needed after the invite is accepted. I've left it to updating theaccepted_at column (remember how that expires the token) and adding the new user to the workspace. But you can add any action that makes sense for your business logic.
And there you have it. A simple way to add invites to your Rails 8 authentication generator.
Top comments(1)

- Email
- LocationAmsterdam, The Netherlands
- Joined
Any next extension of Rails 8' authentication you want to see?
For further actions, you may consider blocking this person and/orreporting abuse