Contexto
Supondo que tenhamos um aplicativo com alguns modelos semelhantes a este:
class User < ActiveRecord::Association
has_many :memberships
has_many :user_groups, through: :memberships
end
class UserGroup < ActiveRecord::Association #(Could be a company, club, circle, etc.)
has_many :memberships
has_many :users, through: :memberships
end
class Membership < ActiveRecord::Association #(Pass through model)
belongs_to :user
belongs_to :user_group
end
Critério
- Um usuário pode convidar alguém para participar de um grupo de usuários, fornecendo um e-mail
- Se o usuário existir, ele será adicionado como membro do grupo de usuários
- Se o usuário não existir, o aplicativo enviará um e-mail com um link para se inscrever e criará automaticamente uma associação para o novo usuário, dando-lhe acesso ao grupo de usuários
- O convite concede ao usuário convidado acesso apenas ao grupo de usuários para o qual foram convidados
Pré-requisitos
- Algum tipo de sistema de autenticação com um modelo de usuário. (Devise, Sorcery)
- Modelos configurados da forma acima mencionada. Os modelos de usuário e grupo de usuários devem ser associados de muitos para muitos. O exemplo acima usa , mas não precisa.
has_many :through
- As permissões configuradas permitem que os usuários vejam os grupos de usuários apenas se forem membros desse grupo. CanCan torna isso maravilhosamente simples.
Começando
Há muitas informações a serem associadas ao convite, então precisamos de um modelo para ele.
Modelos
class Invite < ActiveRecord::Base
belongs_to :user_group
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 UserGroup < ActiveRecord:Base
has_many :invites
end
Migração
class CreateInvites < ActiveRecord::Migration
def change
create_table :invites do |t|
t.string :email
t.integer :user_group_id
t.integer :sender_id
t.integer :recipient_id
t.string :token
t.timestamps
end
end
end
Rotas
resources :invites
Agora temos uma ótima maneira de controlar os convites e, se precisarmos adicionar recursos como limites de convite ou tempo de expiração, podemos fazer isso facilmente.
Vamos criar um formulário rápido para um usuário existente enviar um convite. Coloquei este formulário na visualização de edição do Grupo de usuários, mas ele pode ir a qualquer lugar.
Enviar formulário de convite
<%= form_for @invite , :url => invites_path do |f| %>
<%= f.hidden_field :user_group_id, :value => @invite.user_group_id %>
<%= f.label :email %>
<%= f.email_field :email %>
<%= f.submit 'Send' %>
<% end %>
O formulário possui apenas uma entrada, o e-mail do convidado. Há também um campo oculto que especifica o grupo de usuários ao qual a pessoa está sendo convidada a ter acesso, que é o grupo de usuários atual, já que estou colocando-o na visualização.user_group#edit
Também precisaremos de um Mailer para enviar o e-mail. O mailer do convite é muito básico, então não vou entrar em muitos detalhes aqui, mas ele vai enviar para o do convite recém-criado e incluir uma URL de convite que construiremos mais tarde. O mailer terá 2 métodos, um para enviar o e-mail de convite para novos usuários e outro para enviar um e-mail de notificação para usuários existentes.:email
Fazendo um novo convite
Quando um usuário envia o formulário para fazer um novo convite, não precisamos apenas enviar o convite por email, mas também precisamos gerar um token. O token é usado na URL do convite para (mais) identificar com segurança o convite quando o novo usuário clica para se registrar.
Para gerar um token antes que o convite seja salvo, vamos adicionar um before_create
filtro ao nosso modelo de convite.
before_create :generate_token
def generate_token
self.token = Digest::SHA1.hexdigest([self.user_group_id, Time.now, rand].join)
end
Aqui, estou usando o e a hora atual mais um número aleatório para gerar um token aleatório. Você pode usar o que quiser e, quanto mais complexo for o token, (possivelmente) mais seguros serão os convites.:user_group_id
Portanto, agora, quando criarmos um novo convite, ele gerará o token automaticamente. Agora, em nossa create
ação, precisamos disparar um e-mail de convite (controlado por nosso Mailer), mas SOMENTE se o convite for salvo com sucesso.
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
Aqui, o InviteMailer
leva 2 parâmetros, o convite e o URL do convite, que é constituído da seguinte forma:
new_user_registration_path(:invite_token => @invite.token) #new_user_registration_path is a Devise path. Use the correct registration route for your app
#outputs -> http://yourapp.com/users/sign_up?invite_token=075eeb1ac0165950f9af3e523f207d0204a9efef
Agora, se preenchermos nosso formulário de convite, podemos olhar em nosso log do servidor para ver se um e-mail foi gerado com uma url construída semelhante à acima. Para realmente fazer com que o e-mail seja enviado, você provavelmente precisa configurar um serviço de e-mail de terceiros, como Postmark ou Mandrill .
Registro de usuário recém-convidado
Agora, quando alguém clica no link de convite, é direcionado para a página de registro do seu aplicativo. No entanto, registrar um usuário convidado será um pouco diferente do que registrar um novo usuário. Precisamos anexar este usuário convidado ao grupo de usuários para o qual ele foi convidado durante o registro. É por isso que precisamos do parâmetro token na url, porque agora temos uma maneira de identificar e anexar o usuário ao grupo de usuários correto.
Primeiro, precisamos modificar nosso controlador de registro do usuário para ler o parâmetro do url na new
ação:
def new
@token = params[:invite_token] #<-- pulls the value from the url query string
end
Em seguida, precisamos modificar nossa visão para colocar esse parâmetro em um campo oculto que é enviado quando o usuário envia o formulário de registro. Podemos usar uma instrução condicional na nova visualização de registro para gerar esse campo quando um parâmetro estiver presente na url.:invite_token
<% if @token != nil %>
<%= hidden_field_tag :invite_token, @token %>
<% end %>
Em seguida, precisamos modificar a create
ação do usuário para aceitar este parâmetro não mapeado .:invite_token
def create
@newUser = build_user(user_params)
@newUser.save
@token = params[:invite_token]
if @token != nil
org = Invite.find_by_token(@token).user_group #find the user group attached to the invite
@newUser.user_groups.push(org) #add this user to the new user group as a member
else
# do normal registration things #
end
end
Agora, quando o usuário se registrar, ele automaticamente terá acesso ao grupo de usuários para o qual foi convidado, conforme o esperado.
E se o email já for um usuário registrado?
Não queremos enviar o mesmo e-mail de convite que enviaríamos para um usuário não existente. Este usuário não precisa se registrar novamente, ele já está usando nosso aplicativo, só queremos dar-lhe acesso a outra parte dele. Precisamos adicionar um cheque ao nosso modelo Invite por meio de um before_save
filtro:
before_save :check_user_existence
def check_user_existence
recipient = User.find_by_email(email)
if recipient
self.recipient_id = recipient.id
end
end
Este método irá procurar um usuário com o e-mail enviado e, se for encontrado, irá anexar a ID desse usuário ao convite, já que isso por si só não faz muito. Precisamos modificar nosso controlador Invite para fazer algo diferente se o usuário já existir::recipient_id
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 user group
@invite.recipient.user_groups.push(@invite.user_group)
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
Agora, se o usuário existir, ele se tornará automaticamente um membro do grupo de usuários.
Indo além
Essa foi apenas uma implementação básica, e se você estiver usando isso na produção, você pode adicionar algumas coisas adicionais para proteger melhor o sistema e melhorar a experiência do usuário. Felizmente, como temos um modelo de convite separado, podemos fazer essas coisas com bastante facilidade.
- Adicione uma verificação adicional à ação criar usuário para garantir que o e-mail enviado com o registro corresponda ao do convite do token.
- Adicione um booleano à tabela Invites e permita aos usuários existentes a capacidade de aceitar ou negar um convite, em vez de receber automaticamente acesso ao grupo de usuários.
:accepted
- Dê aos usuários existentes um número limitado de convites que eles podem enviar e verifique o parâmetro que adicionamos no modelo de usuário. (Ajustes adicionais necessários para ter um limite por grupo de usuários)
:sent_invites
- Defina um prazo de validade para os convites e destrua automaticamente os convites após um determinado período de tempo, tornando-os inutilizáveis.
Atualizar
Enviando vários convites de uma vez
Algumas pessoas perguntaram sobre o envio de vários convites de uma vez. Implementei isso no aplicativo atual em que estou trabalhando fazendo o seguinte:
# UserGroupsController
def invite_to
emails = params[:invite_emails].split(', ')
emails.each do |email|
invite = Invite.new(:sender_id => current_user.id, :email => email, user_group_id => @user_group.id)
if invite.save
if invite.recipient != nil
InviteMailer.existing_user_invite(invite).deliver
else
InviteMailer.new_user_invite(invite, new_user_registration_path(:invite_token => @invite.token))
end
end
end