FEATURE: generate invite token

This commit is contained in:
Arpit Jalan 2015-08-26 07:11:52 +05:30
parent 727827dc25
commit 4ad07b8c09
8 changed files with 132 additions and 6 deletions

View File

@ -27,7 +27,7 @@ export default Ember.Controller.extend(ModalFunctionality, {
}.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'), }.property('isAdmin', 'emailOrUsername', 'invitingToTopic', 'isPrivateTopic', 'model.groupNames', 'model.saving'),
buttonTitle: function() { 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'), }.property('model.saving'),
// We are inviting to a topic if the model isn't the current user. // 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; return this.get('model') !== this.currentUser;
}.property('model'), }.property('model'),
invitingToForum: function() {
return (!Discourse.SiteSettings.enable_sso && !this.get('invitingToTopic') && !this.get('isMessage'));
}.property('invitingToTopic', 'isMessage'),
topicId: Ember.computed.alias('model.id'), topicId: Ember.computed.alias('model.id'),
// Is Private Topic? (i.e. visible only to specific group members) // Is Private Topic? (i.e. visible only to specific group members)
@ -95,14 +99,16 @@ export default Ember.Controller.extend(ModalFunctionality, {
}, },
successMessage: function() { 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'); return I18n.t('topic.invite_private.success');
} else if ( Discourse.Utilities.emailValid(this.get('emailOrUsername')) ) { } else if ( Discourse.Utilities.emailValid(this.get('emailOrUsername')) ) {
return I18n.t('topic.invite_reply.success_email', { emailOrUsername: this.get('emailOrUsername') }); return I18n.t('topic.invite_reply.success_email', { emailOrUsername: this.get('emailOrUsername') });
} else { } else {
return I18n.t('topic.invite_reply.success_username'); return I18n.t('topic.invite_reply.success_username');
} }
}.property('isMessage', 'emailOrUsername'), }.property('model.inviteLink', 'isMessage', 'emailOrUsername'),
errorMessage: function() { errorMessage: function() {
return this.get('isMessage') ? I18n.t('topic.invite_private.error') : I18n.t('topic.invite_reply.error'); 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, groupNames: null,
error: false, error: false,
saving: 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); this.get('model.details.allowed_users').pushObject(result.user);
} }
}).catch(() => model.setProperties({ saving: false, error: true })); }).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 }));
} }
} }

View File

@ -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() { updateMutedCategories: function() {
this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids)); this.set("mutedCategories", Discourse.Category.findByIds(this.muted_category_ids));
}.observes("muted_category_ids"), }.observes("muted_category_ids"),

View File

@ -28,6 +28,9 @@
{{#if model.finished}} {{#if model.finished}}
{{d-button class="btn-primary" action="closeModal" label="close"}} {{d-button class="btn-primary" action="closeModal" label="close"}}
{{else}} {{else}}
<button class='btn btn-primary' {{bind-attr disabled="disabled"}} {{action "createInvite"}}>{{fa-icon "user-plus"}}{{buttonTitle}}</button> {{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}} {{/if}}
</div> </div>

View File

@ -4,7 +4,7 @@ class InvitesController < ApplicationController
skip_before_filter :check_xhr, :preload_json skip_before_filter :check_xhr, :preload_json
skip_before_filter :redirect_to_login_if_required 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] before_filter :ensure_new_registrations_allowed, only: [:show, :redeem_disposable_invite]
def show def show
@ -48,6 +48,21 @@ class InvitesController < ApplicationController
end end
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 def create_disposable_invite
guardian.ensure_can_create_disposable_invite!(current_user) guardian.ensure_can_create_disposable_invite!(current_user)
params.permit(:username, :email, :quantity, :group_names) params.permit(:username, :email, :quantity, :group_names)

View File

@ -71,6 +71,7 @@ class Invite < ActiveRecord::Base
end end
end end
end end
# Create an invite for a user, supplying an optional topic # 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. # 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 invite
end 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 # generate invite tokens without email
def self.generate_disposable_tokens(invited_by, quantity=nil, group_names=nil) def self.generate_disposable_tokens(invited_by, quantity=nil, group_names=nil)
invite_tokens = [] invite_tokens = []
@ -153,6 +182,7 @@ class Invite < ActiveRecord::Base
def self.find_all_invites_from(inviter, offset=0, limit=SiteSetting.invites_per_page) def self.find_all_invites_from(inviter, offset=0, limit=SiteSetting.invites_per_page)
Invite.where(invited_by_id: inviter.id) Invite.where(invited_by_id: inviter.id)
.where('invites.email IS NOT NULL')
.includes(:user => :user_stat) .includes(:user => :user_stat)
.order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END', .order('CASE WHEN invites.user_id IS NOT NULL THEN 0 ELSE 1 END',
'user_stats.time_read DESC', 'user_stats.time_read DESC',

View File

@ -625,6 +625,8 @@ en:
days_visited: "Days Visited" days_visited: "Days Visited"
account_age_days: "Account age in days" account_age_days: "Account age in days"
create: "Send an Invite" create: "Send an Invite"
generate_link: "Copy Invite Link"
generated_link_message: '<p>Invite link generated successfully!</p><p><code><b>%{inviteLink}</b></code></p><p>Invite link is only valid for this email address: <b>%{invitedEmail}</b></p>'
bulk_invite: bulk_invite:
none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>uploading a bulk invite file</a>." none: "You haven't invited anyone here yet. You can send individual invites, or invite a bunch of people at once by <a href='https://meta.discourse.org/t/send-bulk-invites/16468'>uploading a bulk invite file</a>."
text: "Bulk Invite from File" text: "Bulk Invite from File"

View File

@ -498,6 +498,7 @@ Discourse::Application.routes.draw do
end end
end end
post "invites/reinvite" => "invites#resend_invite" post "invites/reinvite" => "invites#resend_invite"
post "invites/link" => "invites#create_invite_link"
post "invites/disposable" => "invites#create_disposable_invite" post "invites/disposable" => "invites#create_disposable_invite"
get "invites/redeem/:token" => "invites#redeem_disposable_invite" get "invites/redeem/:token" => "invites#redeem_disposable_invite"
delete "invites" => "invites#destroy" delete "invites" => "invites#destroy"

View File

@ -80,6 +80,49 @@ describe InvitesController do
end 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 '.show' do
context 'with an invalid invite id' do context 'with an invalid invite id' do