From 575b5e3d134cc8dc360ef0a163d50976f6124fe3 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Mon, 14 Jul 2014 21:26:26 +0530 Subject: [PATCH] FEATURE: disposable invite tokens --- app/controllers/invites_controller.rb | 42 +++++++++++++++ app/models/invite.rb | 47 ++++++++++++++-- app/models/invite_redeemer.rb | 20 ++++--- config/routes.rb | 2 + ...140711143146_remove_not_null_from_email.rb | 9 ++++ lib/guardian.rb | 4 ++ spec/controllers/invites_controller_spec.rb | 54 +++++++++++++++++++ spec/models/invite_redeemer_spec.rb | 6 +-- spec/models/invite_spec.rb | 20 ++++++- 9 files changed, 188 insertions(+), 16 deletions(-) create mode 100644 db/migrate/20140711143146_remove_not_null_from_email.rb diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 08b92997f09..7a9db2cdc07 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -41,6 +41,38 @@ class InvitesController < ApplicationController end end + def create_disposable_invite + guardian.ensure_can_create_disposable_invite!(current_user) + params.permit(:username, :email, :quantity, :group_names) + + username_or_email = params[:username] ? fetch_username : fetch_email + user = User.find_by_username_or_email(username_or_email) + + # generate invite tokens + invite_tokens = Invite.generate_disposable_tokens(user, params[:quantity], params[:group_names]) + + render_json_dump(invite_tokens) + end + + def redeem_disposable_invite + params.require(:email) + params.permit(:username, :name) + + invite = Invite.find_by(invite_key: params[:token]) + + if invite.present? + user = Invite.redeem_from_token(params[:token], params[:email], params[:username], params[:name]) + if user.present? + log_on_user(user) + + # Send a welcome message if required + user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message + end + end + + redirect_to "/" + end + def destroy params.require(:email) @@ -95,4 +127,14 @@ class InvitesController < ApplicationController render nothing: true end + def fetch_username + params.require(:username) + params[:username] + end + + def fetch_email + params.require(:email) + params[:email] + end + end diff --git a/app/models/invite.rb b/app/models/invite.rb index f60a69f16ba..1f475a87a10 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -9,15 +9,14 @@ class Invite < ActiveRecord::Base has_many :groups, through: :invited_groups has_many :topic_invites has_many :topics, through: :topic_invites, source: :topic - validates_presence_of :email validates_presence_of :invited_by_id before_create do self.invite_key ||= SecureRandom.hex end - before_save do - self.email = Email.downcase(email) + before_validation do + self.email = Email.downcase(email) unless email.nil? end validate :user_doesnt_already_exist @@ -96,6 +95,37 @@ class Invite < ActiveRecord::Base invite end + + # generate invite tokens without email + def self.generate_disposable_tokens(invited_by, quantity=nil, group_names=nil) + invite_tokens = [] + quantity ||= 1 + group_ids = get_group_ids(group_names) + + quantity.to_i.times do + invite = Invite.create!(invited_by: invited_by) + group_ids = group_ids - invite.invited_groups.pluck(:group_id) + group_ids.each do |group_id| + invite.invited_groups.create!(group_id: group_id) + end + invite_tokens.push(invite.invite_key) + end + + invite_tokens + end + + def self.get_group_ids(group_names) + group_ids = [] + if group_names + group_names = group_names.split(',') + group_names.each { |group_name| + group_detail = Group.find_by_name(group_name) + group_ids.push(group_detail.id) if group_detail + } + end + group_ids + end + def self.find_all_invites_from(inviter) Invite.where(invited_by_id: inviter.id) .includes(:user => :user_stat) @@ -138,6 +168,15 @@ class Invite < ActiveRecord::Base invite end + def self.redeem_from_token(token, email, username=nil, name=nil) + invite = Invite.find_by(invite_key: token) + if invite + invite.update_column(:email, email) + user = InviteRedeemer.new(invite, username, name).redeem + end + user + end + def self.base_directory File.join(Rails.root, "public", "csv", RailsMultisite::ConnectionManagement.current_db) end @@ -153,7 +192,7 @@ end # # id :integer not null, primary key # invite_key :string(32) not null -# email :string(255) not null +# email :string(255) # invited_by_id :integer not null # user_id :integer # redeemed_at :datetime diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb index b4ec857df23..81fb3c218c8 100644 --- a/app/models/invite_redeemer.rb +++ b/app/models/invite_redeemer.rb @@ -1,4 +1,4 @@ -InviteRedeemer = Struct.new(:invite) do +InviteRedeemer = Struct.new(:invite, :username, :name) do def redeem Invite.transaction do @@ -18,19 +18,23 @@ InviteRedeemer = Struct.new(:invite) do end # extracted from User cause it is very specific to invites - def self.create_user_from_invite(invite) - + def self.create_user_from_invite(invite, username, name) user_exists = User.find_by_email(invite.email) return user if user_exists - username = UserNameSuggester.suggest(invite.email) + if username && User.username_available?(username) + available_username = username + else + available_username = UserNameSuggester.suggest(invite.email) + end + available_name = name || available_username DiscourseHub.username_operation do - match, available, suggestion = DiscourseHub.username_match?(username, invite.email) - username = suggestion unless match || available + match, available, suggestion = DiscourseHub.username_match?(available_username, invite.email) + available_username = suggestion unless match || available end - user = User.new(email: invite.email, username: username, name: username, active: true, trust_level: SiteSetting.default_invitee_trust_level) + user = User.new(email: invite.email, username: available_username, name: available_name, active: true, trust_level: SiteSetting.default_invitee_trust_level) user.save! DiscourseHub.username_operation { DiscourseHub.register_username(username, invite.email) } @@ -65,7 +69,7 @@ InviteRedeemer = Struct.new(:invite) do def get_invited_user result = get_existing_user - result ||= InviteRedeemer.create_user_from_invite(invite) + result ||= InviteRedeemer.create_user_from_invite(invite, username, name) result.send_welcome_message = false result end diff --git a/config/routes.rb b/config/routes.rb index c7a5acddf7f..9b99cd384c7 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -375,6 +375,8 @@ Discourse::Application.routes.draw do post "upload" => "invites#upload_csv_chunk" end end + post "invites/disposable" => "invites#create_disposable_invite" + get "invites/redeem/:token" => "invites#redeem_disposable_invite" delete "invites" => "invites#destroy" get "onebox" => "onebox#show" diff --git a/db/migrate/20140711143146_remove_not_null_from_email.rb b/db/migrate/20140711143146_remove_not_null_from_email.rb new file mode 100644 index 00000000000..9cde9b01bce --- /dev/null +++ b/db/migrate/20140711143146_remove_not_null_from_email.rb @@ -0,0 +1,9 @@ +class RemoveNotNullFromEmail < ActiveRecord::Migration + def up + execute "ALTER TABLE invites ALTER COLUMN email DROP NOT NULL" + end + + def down + execute "ALTER TABLE invites ALTER COLUMN email SET NOT NULL" + end +end diff --git a/lib/guardian.rb b/lib/guardian.rb index 62f40d1689c..2f2365566b7 100644 --- a/lib/guardian.rb +++ b/lib/guardian.rb @@ -210,6 +210,10 @@ class Guardian user.admin? end + def can_create_disposable_invite?(user) + user.admin? + end + def can_see_private_messages?(user_id) is_admin? || (authenticated? && @user.id == user_id) end diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index 436a0dc27fd..86a32d68cd6 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -150,6 +150,60 @@ describe InvitesController do end + context '.create_disposable_invite' do + it 'requires you to be logged in' do + lambda { + post :create, email: 'jake@adventuretime.ooo' + }.should raise_error(Discourse::NotLoggedIn) + end + + context 'while logged in as normal user' do + let(:user) { Fabricate(:user) } + + it "does not create disposable invitation" do + log_in + post :create_disposable_invite, email: user.email + response.should_not be_success + end + end + + context 'while logged in as admin' do + before do + log_in(:admin) + end + let(:user) { Fabricate(:user) } + + it "creates disposable invitation" do + post :create_disposable_invite, email: user.email + response.should be_success + Invite.where(invited_by_id: user.id).count.should == 1 + end + + it "creates multiple disposable invitations" do + post :create_disposable_invite, email: user.email, quantity: 10 + response.should be_success + Invite.where(invited_by_id: user.id).count.should == 10 + end + + it "allows group invite" do + group = Fabricate(:group) + post :create_disposable_invite, email: user.email, group_names: group.name + response.should be_success + Invite.find_by(invited_by_id: user.id).invited_groups.count.should == 1 + end + + it "allows multiple group invite" do + group_1 = Fabricate(:group, name: "security") + group_2 = Fabricate(:group, name: "support") + post :create_disposable_invite, email: user.email, group_names: "security,support" + response.should be_success + Invite.find_by(invited_by_id: user.id).invited_groups.count.should == 2 + end + + end + + end + context '.check_csv_chunk' do it 'requires you to be logged in' do lambda { diff --git a/spec/models/invite_redeemer_spec.rb b/spec/models/invite_redeemer_spec.rb index acccfcdee29..b8992ac76ff 100644 --- a/spec/models/invite_redeemer_spec.rb +++ b/spec/models/invite_redeemer_spec.rb @@ -3,10 +3,10 @@ require 'spec_helper' describe InviteRedeemer do describe '#create_for_email' do - let(:user) { InviteRedeemer.create_user_from_invite(Fabricate(:invite, email: 'walter.white@email.com')) } + let(:user) { InviteRedeemer.create_user_from_invite(Fabricate(:invite, email: 'walter.white@email.com'), 'walter', 'Walter White') } it "should be created correctly" do - user.username.should == 'walter_white' - user.name.should == 'walter_white' + user.username.should == 'walter' + user.name.should == 'Walter White' user.should be_active user.email.should == 'walter.white@email.com' end diff --git a/spec/models/invite_spec.rb b/spec/models/invite_spec.rb index 82704203fbb..ac1b86a76ce 100644 --- a/spec/models/invite_spec.rb +++ b/spec/models/invite_spec.rb @@ -2,7 +2,6 @@ require 'spec_helper' describe Invite do - it { should validate_presence_of :email } it { should validate_presence_of :invited_by_id } let(:iceking) { 'iceking@adventuretime.ooo' } @@ -369,4 +368,23 @@ describe Invite do end + describe '.redeem_from_token' do + let(:inviter) { Fabricate(:user) } + let(:invite) { Fabricate(:invite, invited_by: inviter, email: 'test@example.com', user_id: nil) } + let(:user) { Fabricate(:user, email: invite.email) } + + it 'redeems the invite from token' do + result = Invite.redeem_from_token(invite.invite_key, user.email) + invite.reload + invite.should be_redeemed + end + + it 'does not redeem the invite if token does not match' do + result = Invite.redeem_from_token("bae0071f995bb4b6f756e80b383778b5", user.email) + invite.reload + invite.should_not be_redeemed + end + + end + end