Invitation
A Rails gem to issue scoped invitations.
Please see the CHANGELOG for information on the latest changes.
Please use GitHub Issues to report bugs. You can contact me directly on twitter at @JustinTomich.
Overview
Allow users to invite others to join an organization or resource. Plenty of gems can issue a 'system-wide' invitation, but few offer 'scoped' invitations, giving an invited user access to a particular invitable organization or resource.
Invitations are issued via email. You can invite users new to join the system while giving them permissions to a resource, or invite existing users by giving them access to a new resource.
- a user can invite someone to join an invitable by providing an email address to invite
- if the user already exists, that user is granted access to the invitable, and a notification email is sent
- if the user does not exist, sends an email with a link to sign up. When the new user signs up, they are added to the invitable resource/organization.
- the invite grants the invited user access to ONLY the invitable organization they were invited to.
Prerequisites
- An authentication system with a User model and current_user helper, e.g. https://github.com/tomichj/authenticate
- Your user model must include an :email attribute.
- Additional model classes that are resources or organizations you wish to invite users to join, usually with a many-to-many relationship to your user model.
- You probably also want an authorization system to restrict who can issue invitations to particular resources
A example user-to-organization system you might be familiar with: Basecamp's concepts of accounts and projects (invitables) and users.
Install
To get started, add Invitation to your Gemfile
and run bundle install
to install it:
gem 'invitation'
Then run the invitation install generator:
rails generate invitation:install
If your user model is not User, you can optionally specify one: rails generate invitation:install --model Profile
.
The install generator does the following:
- Add an initializer at
config/initializers/invitation.rb
, see Configure below. - Insert
include Invitation::User
into yourUser
model. - Create a migration for the Invite class.
Then run the migration that Invitation just generated.
rake db:migrate
Configure
Override any of these defaults in your application config/initializers/invitation.rb
.
Invitation.configure do |config|
config.user_model = '::User'
config.user_registration_url = ->(params) { Rails.application.routes.url_helpers.sign_up_url(params) }
config.mailer_sender = 'reply@example.com'
config.routes = true
config.case_sensitive_email = true
end
Configuration parameters are described in detail here: configuration
Invitable
You'll need to configure one or more model classes as invitables
. Invitables are resources or organizations that
can be joined with an invite.
An invitable
must have some sort of name for Invitation to use in views and mailers. An invitable needs to
call a class method, invitable
, with one of the following options:
named: "String"
-
named_by: :some_method_name
.
Example: a Company model that users can be invited to join. The companies are identified in invitation emails by
their name
attribute:
class Company < ActiveRecord::Base
invitable named_by: :name
end
User Registration Controller
Your user registration controller must include Invitation::UserRegistration
. You'll want to invoke set_invite_token
before you execute your new
action, and process_invite_token
after your create
action.
If you're using Authenticate, for example:
class UsersController < Authenticate::UsersController
include Invitation::UserRegistration
before_action :set_invite_token, only: [:new]
after_action :process_invite_token, only: [:create]
end
To pass the invite token on signup, add invite_token
as a hidden field in your signup form.
Usage
Invitation adds routes to create invitations (GET new_invite and POST invites). Once you've configured Invitation and set up an invitable, add a link to new_invite, specifying the the invitable id and type in the link:
<%= link_to 'invite a friend',
new_invite_path(invite: { invitable_id: account.id, invitable_type: 'Account' } ) %>
Invitation includes a simple invitations#new
view which accepts an email address for a user to invite.
When the form is submitted, invites#create will create an invite to track the invitation. An email is then sent:
-
a new user is emailed a link to your user registration page as set in configuration, with a secure invitation link that will be used to 'claim' the invitation when the new user registers
-
an existing user is emailed a notification to tell them that they've been added to the resource
JSON Invitation
You can send a JSON request to invites#create.
- request:
invite:
{
"email": String,
"invitable_id": Number,
"invitable_type": String
}
or, with an array of multiple emails
:
invite:
{
"emails": [String],
"invitable_id": Number,
"invitable_type": String
}
- response:
{
"id": Number,
"email": String,
"sender_id": Number,
"recipient_id": Number (optional),
"invitable_id": Number,
"invitable_type": String,
}
or, with multiple emails requested, an array of responses:
[{
"id": Number,
"email": String,
"sender_id": Number,
"recipient_id": Number (optional),
"invitable_id": Number,
"invitable_type": String,
},
{
"id": Number,
"email": String,
"sender_id": Number,
"recipient_id": Number (optional),
"invitable_id": Number,
"invitable_type": String,
}]
Security
Many systems require authorization to issue invitations to a resource. Invitation
does not handle authorization,
but is intended to be a simple, easily extended framework upon which you can impose any required authorization scheme.
Most implementations will require extending the InvitationsController
. See below to read more
about extending InvitationController.
A common use case:
- every invitable resource has authorization requirements exposed via a method, we'll call it
can_invite?(user)
- the current user must be authorized before issuing invitations
To implement: extend InvitesController
and add a before_action to authorize access to the resource or resources. A
real implementation would probably do something more than just raise 'unauthorized'
.
# app/controllers/invites_controller.rb
class InvitesController < Invitation::InvitesController
before_action :authorize
private
def authorize
invitable = load_invitable
invitable.can_invite?(current_user) or raise 'unauthorized'
end
def load_invitable
invite_params[:invitable_type].classify.constantize.find(invite_params[:invitable_id])
end
end
Overriding Invitation
Views
You can quickly get started with a rails application using the built-in views. See app/views for the default views. When you want to customize an Invitation view, create your own copy of it in your app.
You can use the Invitation view generator to copy the default views and translations (see translations below) into your application:
$ rails generate invitation:views
Routes
Invitation adds routes to your application. See config/routes.rb for the default routes.
If you want to control and customize the routes, you can turn off the built-in routes in
the Invitation configuration with config.routes = false
and dump a copy of the default routes into your
application for modification.
To turn off Invitation's built-in routes:
Invitation.configure do |config|
config.routes = false
end
You can run a generator to dump a copy of the default routes into your application for modification. The generator
will also switch off the routes by setting config.routes = false
in your configuration.
$ rails generate invitation:routes
Controllers
You can customize the invites_controller.rb
and the invites_mailer.rb
. See app/controllers
for the controller, and app/mailers for the mailer.
To override invites_controller.rb
, subclass the controller and update your routes to point to your implementation.
- subclass the controller:
# app/controllers/invites_controller.rb
class InvitesController < Invitation::InvitesController
# render invite screen
def new
# ...
end
...
end
- update your routes to use your new controller.
Start by dumping a copy of Invitation's routes to your config/routes.rb
:
$ rails generate invitation:routes
Now update config/routes.rb
, changing the controller entry so it now points to your invites
controller instead
of invitation/invites
:
resources :invites, controller: 'invites', only: [:new, :create]
You can also use the Invitation controller generator to copy the default controller and mailer into your application if you would prefer to more heavily modify the controller.
$ rails generate invitation:controllers
Layout
Invitation uses your application's default layout. If you would like to change the layout Invitation uses when
rendering views, you can either deploy copies of the controllers and customize them, or you can specify
the layout in an initializer. This should be done in a to_prepare callback in config/application.rb
because it's executed once in production and before each request in development.
You can specify the layout per-controller:
config.to_prepare do
Invitation::InvitesController.layout 'my_invites_layout'
end
Translations
All flash messages and email subject lines are stored in i18n translations. Override them like any other i18n translation.
See config/locales/invitation.en.yml for the default messages.
Thanks
This gem was inspired by and draws heavily from:
With additional inspiration from:
Contributors
Many thanks to:
- augustocbx added pt-BR locale file and fixed an init bug
- vincentwoo raising the security bar, & bumping Invitation to rails 5.1
- conarro added case_sensitive_email configuration option
- itkin bugfix, stringified configuration.user_model
- thesubr00t made recipient association optional for rails 5+
Future changes
Possible future changes, some of which may be breaking:
- accepted flag, so we can scope invites by accepted vs not yet accepted
- expiration date - invites expire, scope expired by not expired
- move all view text to locale
- issue many invitations at once?
- dynamic user name lookup? requires JS, CSS
- add JS support to invites#create
License
This project rocks and uses MIT-LICENSE.