Last active
June 16, 2025 21:11
-
-
Save bigglesrocks/9026919 to your computer and use it in GitHub Desktop.
Revisions
-
Jessica Biggs revised this gist
Feb 24, 2014 . 1 changed file with 1 addition and 2 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -19,8 +19,7 @@ Starting out with the following models and associations: ##The Issue 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, however when you don't have any app-wide views or functions, this presents an issue. ##Criteria - A user can invite someone to join an organization by providing an email -
Jessica Biggs revised this gist
Feb 24, 2014 . 1 changed file with 1 addition and 7 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,12 +1,6 @@ #Scoped Invitation System for User Groups with Rails# Starting out with the following models and associations: ####User - has_many **:memberships** -
Jessica Biggs revised this gist
Feb 16, 2014 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -124,7 +124,7 @@ So now when we create a new invite, it will generate the token automagically. No end end Here the `InviteMailer` takes 2 parameters, the invite and the invite URL which is consrtucted thusly: new_user_registration_path(:invite_token => @invite.token) #outputs -> http://yourapp.com/users/sign_up?invite_token=075eeb1ac0165950f9af3e523f207d0204a9efef -
Jessica Biggs revised this gist
Feb 15, 2014 . 1 changed file with 1 addition and 1 deletion.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,4 +1,4 @@ #Scoped Invitation System for User Groups with Rails# ##ProjectList## -
Jessica Biggs revised this gist
Feb 15, 2014 . 1 changed file with 2 additions and 0 deletions.There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -1,3 +1,5 @@ #Scoped Invitation System for User Groupe with Rails# ##ProjectList## So, I just recently wrote my very first Rails app. Instead of trying to tackle an overly simplified application that wasn't suited for production, I started my path into rails by tackling the production version of a [Simple Focus](http://www.simplefocus.com) product, ProjectList. -
Jessica Biggs revised this gist
Feb 15, 2014 . 4 changed files with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes.File renamed without changes.File renamed without changes.File renamed without changes. -
Jessica Biggs renamed this gist
Feb 15, 2014 . 1 changed file with 0 additions and 0 deletions.There are no files selected for viewing
File renamed without changes. -
Jessica Biggs created this gist
Feb 15, 2014 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,11 @@ 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 This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,208 @@ ##ProjectList## So, I just recently wrote my very first Rails app. Instead of trying to tackle an overly simplified application that wasn't suited for production, I started my path into rails by tackling the production version of a [Simple Focus](http://www.simplefocus.com) product, ProjectList. ProjectList looks simple, but for a beginner, it's more complex than it appears. As you can imagine, I ran into quite a few issues that installing and configuring another gem wasn't going to solve for me. ProjectList has users that are part of user groups called Organizations. A user can be a member of multiple organizations and an organization can have multiple users. The models for those look a little like this. ####User - has_many **:memberships** - has_many **:organizations** through **:memberships** ####Organization (User Group) - has_many **:memberships** - has_many **:users** through **:memberships** - has_one *owner* (**:user**) ####Membership (Pass through model) - belongs_to **:user** - belongs_to **:organization** ##The Issue 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. ##Criteria - A user can invite someone to join an organization by providing an email - If the user exists, that user is added as a member of the organization - If the user does not exist, the app sends an email with a link to sign up, and automatically creates a membership for the new user - The invitation grants the invited user access to *only* the organization they were invited to ##Prerequisites - Some sort of Authentication system with a User model. I used Devise. - A second model for the User Group that is associated with the user model in a many-to-many way. I've used `has_many :through` with a third model. Perhaps polymorphic associations could also be used? ##Getting Started There's a lot of information to be associated with the invitation, so we need a model for it. **Models** class Invite < ActiveRecord::Base belongs_to :organization belongs_to :sender, :class_name => 'User' belongs_to :recipient, :class_name => 'User' end class User < AciveRecord::Base has_many :invitations, :class_name => "Invite", :foreign_key => 'recipient_id' has_many :sent_invites, :class_name => "Invite", :foreign_key => 'sender_id' end class Organization < ActiveRecord:Base has_many :invites end **Migration** 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 **Routes** resources :invites 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. Let's create a quick form for an existing user to send an invite. I put this form on the edit view for the organziation, but it could go anywhere. **Send Invitation Form** <%= 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 %> The form only has one input, the email of the person being invited. There is also a hidden field that specifies the organization that the person is being invited to have access to, which is the current organization since I'm placing it on the `organizations#edit` view. We'll also need a Mailer to send the email. Mailers are great, and I'm sure you guys know all about them. The invitation mailer is very basic, so I'm not going to go into details here, but it will send to the `:email` of the newly created invitation and include an invite URL that we will construct later. ##Making a New Invitation 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. To generate a token before the invite is saved, let's add a `before_create` filter to our Invite model. before_create :generate_token def generate_token self.token = Digest::SHA1.hexdigest([self.organization_id, Time.now, rand].join) end Here, I'm using the `:organization_id` and the current time plus a random number to generate a random token. So now when we create a new invite, it will generate the token automagically. 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 contstructed thusly: new_user_registration_path(:invite_token => @invite.token) #outputs -> http://yourapp.com/users/sign_up?invite_token=075eeb1ac0165950f9af3e523f207d0204a9efef Now if we fill out our invitation form, we can look in our server log to see that an email was sent with a constructed url like so. ##Newly Invited user registration Now when someone clicks on the invite link, they'r taken to the registration page for your app. However, registering an invited user is going to be a little different than registering a brand new user. We need to attach this invited user to the organization they were invited to during registration. That's why we need the token parameter in the url, because now we have a way to identify and attach the user to the correct organization. First, we need to modify our user registration controller to read the parameter from the url in the `new` action: 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. ##What if the email is already a registered user? We don't want to send the same invitation email that we would for a non-existing user. This user doesn't need to register again, they're already using our app, we just want to give them access to another part of it. We need to add a check to our 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` That in and of itself does not do much. We need to modify our 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. ##Other features - Add an `:accepted` boolean to the Invites table, and allow existing users the ability to accept or deny an invitation. - Add a check in the user registration to validate not only the token but that the email the user is registering with matches the one attached to the invite. This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,24 @@ <%= form_tag @user, :action => 'create', :url => registration_path(@user), :html => { :multipart => true } %> <% if @token != nil %> <%= hidden_field_tag :invite_token, @token %> <% end %> <%= label_tag :user, 'First Name' %><br /> <%= text_field :user, :first_name %> <%= label_tag :user, 'Last Name' %><br /> <%= text_field :user, :last_name %> <%= label_tag :user, 'Email' %><br /> <%= email_field :user, :email, value: @invite.email %> <%= label_tag :user, 'Choose Password' %><br /> <%= password_field :user, :password' %> <%= label_tag :user, 'Confirm Password' %><br /> <%= password_field :user, :password_confirmation %> <%= submit_tag "Sign up" %> <% end %> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,6 @@ <%= 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 %> This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,19 @@ class Invite < ActiveRecord::Base belongs_to :organization belongs_to :sender, :class_name => 'User' belongs_to :recipient, :class_name => 'User' before_create :generate_token before_save :check_user_existence def generate_token self.token = Digest::SHA1.hexdigest([self.organization_id, Time.now, rand].join) end def check_user_existence recipient = User.find_by_email(email) if recipient self.recipient_id = recipient.id end end end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,23 @@ class InvitesController < ApplicationController 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 end This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,11 @@ 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