Scoped User Group Invitation System for Rails
By Jessica Biggs
ProjectList's Models look a little like this
Conceptually, users with appropriate permissions should be able to invite other users, either existing or by email, to join an organization they are a part of. There are plenty of gems out there that take care of application-wide invitation systems, but ProjectList doesn't have any app-wide views or functions. Everything is based within the scope of an organization, so not only did these gems not work for my problem, but they weren't even a good starting point.
has_many :throughwith a third model. Perhaps polymorphic associations could also be used?
In addition to the following, you will need to set up a very basic mailer and add :invites to your routes
There's a lot of information to be associated with the invitation, so we need a model for it.
class Invite < ActiveRecord::Base belongs_to :organization belongs_to :sender, :class_name => 'User' belongs_to :recipient, :class_name => 'User' end
class CreateInvites < ActiveRecord::Migration def change create_table :invites do |t| t.string :email t.integer :sender_id t.integer :recipient_id t.string :token t.timestamps end end end
Now we have a nice way of keeping track of invitations, and if we need to add features like invitation limits or expiration time, we can do so easily.
<%= form_for @invite , :url => invites_path do |f| %> <%= f.hidden_field :organization_id, :value => @invite.organization_id %> <%= f.label :email %> <%= f.email_field :email %> <%= f.submit 'Send' %> <% end %>
When a user submits the form to make a new invite, we not only need to send the email invite, but we need to generate a token as well. The token is used in the invite URL to (more) securely identify the invite when the new user clicks to register.
before_create :generate_token def generate_token self.token = Digest::SHA1.hexdigest([self.organization_id, Time.now, rand].join) end
Now, in our
create action we need to fire off an invite email (controlled by our Mailer), but ONLY if the invite saved successfully.
def create @invite = Invite.new(invite_params) # Make a new Invite @invite.sender_id = current_user.id # set the sender to the current user if @invite.save InviteMailer.new_user_invite(@invite, new_user_registration_path(:invite_token => @invite.token)).deliver #send the invite data to our mailer to deliver the email else # oh no, creating an new invitation failed end end
Here the InviteMailer takes 2 parameters, the invite and the invite URL which is constructed thusly:
new_user_registration_path(:invite_token => @invite.token) #outputs -> http://yourapp.com/users/sign_up?invite_token=075eeb1ac0165950f9af3e523f207d0204a9efef
Registering an invited user is a little different than a brand new user
First, we need to modify our user registration controller to read the parameter from the url
def new @token = params[:invite_token] #<-- pulls the value from the url query string end
Next we need to modify our view to put that parameter into a hidden field that gets submitted when the user submits the registration form. I used a conditional statement within my users#new view to output this field when an :invite_token parameter is present in the url.
<% if @token != nil %> <%= hidden_field_tag :invite_token, @token %> <% end %>
Next we need to modify the user
create action to accept this unmapped :invite_token parameter.
def create @newUser = build_user(user_params) @newUser.save @token = params[:invite_token] if @token != nil org = Invite.find_by_token(@token).organization #find the organization attached to the invite @newUser.organizations.push(org) #add this user to the new organization as a member else # do normal registration things # end end
Now when the user registers, they'll automatically have access to the organization they were invited to, as expected.
Add a check to the Invite model via a before_save filter:
before_save :check_user_existence def check_user_existence recipient = User.find_by_email(email) if recipient self.recipient_id = recipient.id end end
This method will look for a user with the submitted email, and if found it will attach that user's ID to the invitation as the :recipient_id
Modify the Invite controller to do something different if the user already exists:
def create @invite = Invite.new(invite_params) @invite.sender_id = current_user.id if @invite.save #if the user already exists if @invite.recipient != nil #send a notification email InviteMailer.existing_user_invite(@invite).deliver #Add the user to the organization @invite.recipient.organizations.push(@invite.organization) else InviteMailer.new_user_invite(@invite, new_user_registration_path(:invite_token => @invite.token)).deliver end else # oh no, creating an new invitation failed end end
Now if the user exists, he/she wil automatically become a member of the organization.
By Jessica Biggs