From 4ad07b8c09068dad64ae0221b8e888e047b17bc6 Mon Sep 17 00:00:00 2001 From: Arpit Jalan Date: Wed, 26 Aug 2015 07:11:52 +0530 Subject: [PATCH] FEATURE: generate invite token --- .../discourse/controllers/invite.js.es6 | 33 ++++++++++++-- .../javascripts/discourse/models/user.js.es6 | 7 +++ .../discourse/templates/modal/invite.hbs | 5 ++- app/controllers/invites_controller.rb | 17 +++++++- app/models/invite.rb | 30 +++++++++++++ config/locales/client.en.yml | 2 + config/routes.rb | 1 + spec/controllers/invites_controller_spec.rb | 43 +++++++++++++++++++ 8 files changed, 132 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/discourse/controllers/invite.js.es6 b/app/assets/javascripts/discourse/controllers/invite.js.es6 index b9173b87579..fa951677383 100644 --- a/app/assets/javascripts/discourse/controllers/invite.js.es6 +++ b/app/assets/javascripts/discourse/controllers/invite.js.es6 @@ -27,7 +27,7 @@ export default Ember.Controller.extend(ModalFunctionality, { }.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'), buttonTitle: function() { - return this.get('model.saving') ? I18n.t('topic.inviting') : I18n.t('topic.invite_reply.action'); + return this.get('model.saving') ? 'topic.inviting' : 'topic.invite_reply.action'; }.property('model.saving'), // We are inviting to a topic if the model isn't the current user. @@ -36,6 +36,10 @@ export default Ember.Controller.extend(ModalFunctionality, { return this.get('model') !== this.currentUser; }.property('model'), + invitingToForum: function() { + return (!Discourse.SiteSettings.enable_sso && !this.get('invitingToTopic') && !this.get('isMessage')); + }.property('invitingToTopic', 'isMessage'), + topicId: Ember.computed.alias('model.id'), // Is Private Topic? (i.e. visible only to specific group members) @@ -95,14 +99,16 @@ export default Ember.Controller.extend(ModalFunctionality, { }, successMessage: function() { - if (this.get('isMessage')) { + if (this.get('model.inviteLink')) { + return I18n.t('user.invited.generated_link_message', {inviteLink: this.get('model.inviteLink'), invitedEmail: this.get('emailOrUsername')}); + } else if (this.get('isMessage')) { return I18n.t('topic.invite_private.success'); } else if ( Discourse.Utilities.emailValid(this.get('emailOrUsername')) ) { return I18n.t('topic.invite_reply.success_email', { emailOrUsername: this.get('emailOrUsername') }); } else { return I18n.t('topic.invite_reply.success_username'); } - }.property('isMessage', 'emailOrUsername'), + }.property('model.inviteLink', 'isMessage', 'emailOrUsername'), errorMessage: function() { return this.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error'); @@ -121,7 +127,8 @@ export default Ember.Controller.extend(ModalFunctionality, { groupNames: null, error: false, saving: false, - finished: false + finished: false, + inviteLink: null }); }, @@ -147,6 +154,24 @@ export default Ember.Controller.extend(ModalFunctionality, { this.get('model.details.allowed_users').pushObject(result.user); } }).catch(() => model.setProperties({ saving: false, error: true })); + }, + + generateInvitelink() { + if (this.get('disabled')) { return; } + + const groupNames = this.get('model.groupNames'), + userInvitedController = this.get('controllers.user-invited-show'), + model = this.get('model'); + + model.setProperties({ saving: true, error: false }); + + return this.get('model').generateInviteLink(this.get('emailOrUsername').trim(), groupNames).then(result => { + model.setProperties({ saving: false, finished: true, inviteLink: result }); + Invite.findInvitedBy(this.currentUser, userInvitedController.get('filter')).then(invite_model => { + userInvitedController.set('model', invite_model); + userInvitedController.set('totalInvites', invite_model.invites.length); + }); + }).catch(() => model.setProperties({ saving: false, error: true })); } } diff --git a/app/assets/javascripts/discourse/models/user.js.es6 b/app/assets/javascripts/discourse/models/user.js.es6 index 079c508286a..8e3d15e2934 100644 --- a/app/assets/javascripts/discourse/models/user.js.es6 +++ b/app/assets/javascripts/discourse/models/user.js.es6 @@ -372,6 +372,13 @@ const User = RestModel.extend({ }); }, + generateInviteLink: function(email, groupNames) { + return Discourse.ajax('/invites/link', { + type: 'POST', + data: {email: email, group_names: groupNames} + }); + }, + updateMutedCategories: function() { this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids)); }.observes("muted_category_ids"), diff --git a/app/assets/javascripts/discourse/templates/modal/invite.hbs b/app/assets/javascripts/discourse/templates/modal/invite.hbs index cba7a89b4e4..0f41fb3973b 100644 --- a/app/assets/javascripts/discourse/templates/modal/invite.hbs +++ b/app/assets/javascripts/discourse/templates/modal/invite.hbs @@ -28,6 +28,9 @@ {{#if model.finished}} {{d-button class="btn-primary" action="closeModal" label="close"}} {{else}} - + {{d-button icon="envelope" action="createInvite" class="btn-primary" disabled=disabled label=buttonTitle}} + {{#if invitingToForum}} + {{d-button icon="link" action="generateInvitelink" class="btn-primary" disabled=disabled label='user.invited.generate_link'}} + {{/if}} {{/if}} diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb index 3a7f6437f9f..c8d5679e76c 100644 --- a/app/controllers/invites_controller.rb +++ b/app/controllers/invites_controller.rb @@ -4,7 +4,7 @@ class InvitesController < ApplicationController skip_before_filter :check_xhr, :preload_json skip_before_filter :redirect_to_login_if_required - before_filter :ensure_logged_in, only: [:destroy, :create, :resend_invite, :check_csv_chunk, :upload_csv_chunk] + before_filter :ensure_logged_in, only: [:destroy, :create, :create_invite_link, :resend_invite, :check_csv_chunk, :upload_csv_chunk] before_filter :ensure_new_registrations_allowed, only: [:show, :redeem_disposable_invite] def show @@ -48,6 +48,21 @@ class InvitesController < ApplicationController end end + def create_invite_link + params.require(:email) + group_ids = Group.lookup_group_ids(params) + guardian.ensure_can_invite_to_forum!(group_ids) + + invite_exists = Invite.where(email: params[:email], invited_by_id: current_user.id).first + if invite_exists + guardian.ensure_can_send_multiple_invites!(current_user) + end + + # generate invite link + invite_link = Invite.generate_invite_link(params[:email], current_user, group_ids) + render_json_dump(invite_link) + end + def create_disposable_invite guardian.ensure_can_create_disposable_invite!(current_user) params.permit(:username, :email, :quantity, :group_names) diff --git a/app/models/invite.rb b/app/models/invite.rb index 6929084fcab..6bc76bfaeaa 100644 --- a/app/models/invite.rb +++ b/app/models/invite.rb @@ -71,6 +71,7 @@ class Invite < ActiveRecord::Base end end end + # Create an invite for a user, supplying an optional topic # # Return the previously existing invite if already exists. Returns nil if the invite can't be created. @@ -121,6 +122,34 @@ class Invite < ActiveRecord::Base invite end + # generate invite link + def self.generate_invite_link(email, invited_by, group_ids=nil) + lower_email = Email.downcase(email) + + invite = Invite.with_deleted + .where(email: lower_email, invited_by_id: invited_by.id) + .order('created_at DESC') + .first + + if invite && (invite.expired? || invite.deleted_at) + invite.destroy + invite = nil + end + + if !invite + invite = Invite.create!(invited_by: invited_by, email: lower_email) + end + + if group_ids.present? + 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 + end + + return "#{Discourse.base_url}/invites/#{invite.invite_key}" + end + # generate invite tokens without email def self.generate_disposable_tokens(invited_by, quantity=nil, group_names=nil) invite_tokens = [] @@ -153,6 +182,7 @@ class Invite < ActiveRecord::Base def self.find_all_invites_from(inviter, offset=0, limit=SiteSetting.invites_per_page) Invite.where(invited_by_id: inviter.id) + .where('invites.email IS NOT NULL') .includes(:user => :user_stat) .order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END', 'user_stats.time_read DESC', diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 0246fec15f8..06f4f543f34 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -625,6 +625,8 @@ en: days_visited: "Days Visited" account_age_days: "Account age in days" create: "Send an Invite" + generate_link: "Copy Invite Link" + generated_link_message: '

Invite link generated successfully!

%{inviteLink}

Invite link is only valid for this email address: %{invitedEmail}

' bulk_invite: none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by uploading a bulk invite file." text: "Bulk Invite from File" diff --git a/config/routes.rb b/config/routes.rb index 86553f03284..3675a5e769a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -498,6 +498,7 @@ Discourse::Application.routes.draw do end end post "invites/reinvite" => "invites#resend_invite" + post "invites/link" => "invites#create_invite_link" post "invites/disposable" => "invites#create_disposable_invite" get "invites/redeem/:token" => "invites#redeem_disposable_invite" delete "invites" => "invites#destroy" diff --git a/spec/controllers/invites_controller_spec.rb b/spec/controllers/invites_controller_spec.rb index f5ce32a6ff1..7acdf7874ea 100644 --- a/spec/controllers/invites_controller_spec.rb +++ b/spec/controllers/invites_controller_spec.rb @@ -80,6 +80,49 @@ describe InvitesController do end + context '.create_invite_link' do + it 'requires you to be logged in' do + expect { + post :create_invite_link, email: 'jake@adventuretime.ooo' + }.to raise_error(Discourse::NotLoggedIn) + end + + context 'while logged in' do + let(:email) { 'jake@adventuretime.ooo' } + + it "fails if you can't invite to the forum" do + log_in + post :create_invite_link, email: email + expect(response).not_to be_success + end + + it "fails for normal user if invite email already exists" do + user = log_in(:trust_level_4) + invite = Invite.invite_by_email("invite@example.com", user) + invite.reload + post :create_invite_link, email: invite.email + expect(response).not_to be_success + end + + it "allows admins to invite to groups" do + group = Fabricate(:group) + log_in(:admin) + post :create_invite_link, email: email, group_names: group.name + expect(response).to be_success + expect(Invite.find_by(email: email).invited_groups.count).to eq(1) + end + + it "allows multiple group invite" do + group_1 = Fabricate(:group, name: "security") + group_2 = Fabricate(:group, name: "support") + log_in(:admin) + post :create_invite_link, email: email, group_names: "security,support" + expect(response).to be_success + expect(Invite.find_by(email: email).invited_groups.count).to eq(2) + end + end + end + context '.show' do context 'with an invalid invite id' do