mirror of
https://github.com/discourse/discourse.git
synced 2025-02-10 21:34:50 +00:00
* FIX: Do not show expired invites under Pending tab * DEV: Controller action was renamed in previous commit * FEATURE: Add 'Expired' tab to invites * FEATURE: Refresh model after removing expired invites * FEATURE: Do not immediately add invite to the list Opening the 'create-invite' modal used to automatically generate an invite to reserve an invite link. If the user did not save it and closed the modal, the invite would be destroyed. This operations caused the invite list to change in the background and confuse users. * FEATURE: Sort redeemed users by creation time * UX: Improve show / hide advanced options link * FIX: Show redeemed users even if invites were trashed * UX: Change modal title when editing invite * UX: Remove Get Link button Users can get it from the edit modal * FEATURE: Add limit for invite links generated by regular users * FEATURE: Add option to skip email * UX: Show better error messages * FIX: Show "Invited by" even if invite was trashed Follow up to 1fdfa13a099d8e46edd0c481b3aaaafe40455ced. * FEATURE: Add button to save without sending email Follow up to c86379a465f28a3cc64a4a8c939cf32cf2931659. * DEV: Use a buffer to hold all changed data * FEATURE: Close modal after save * FEATURE: Rate limit resend invite email * FEATURE: Make the save buttons smarter * FEATURE: Do not always send email even for new invites
325 lines
10 KiB
Ruby
325 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
class InvitesController < ApplicationController
|
|
|
|
requires_login only: [:create, :destroy, :destroy_all_expired, :resend_invite, :resend_all_invites, :upload_csv]
|
|
|
|
skip_before_action :check_xhr, except: [:perform_accept_invitation]
|
|
skip_before_action :preload_json, except: [:show]
|
|
skip_before_action :redirect_to_login_if_required
|
|
|
|
before_action :ensure_new_registrations_allowed, only: [:show, :perform_accept_invitation]
|
|
before_action :ensure_not_logged_in, only: [:show, :perform_accept_invitation]
|
|
|
|
def show
|
|
expires_now
|
|
|
|
invite = Invite.find_by(invite_key: params[:id])
|
|
if invite.present? && !invite.expired? && !invite.redeemed?
|
|
store_preloaded("invite_info", MultiJson.dump(
|
|
invited_by: UserNameSerializer.new(invite.invited_by, scope: guardian, root: false),
|
|
email: invite.email,
|
|
username: UserNameSuggester.suggest(invite.email),
|
|
is_invite_link: invite.is_invite_link?
|
|
))
|
|
|
|
render layout: 'application'
|
|
else
|
|
flash.now[:error] = if invite&.expired?
|
|
I18n.t('invite.expired', base_url: Discourse.base_url)
|
|
elsif invite&.redeemed?
|
|
I18n.t('invite.not_found_template', site_name: SiteSetting.title, base_url: Discourse.base_url)
|
|
else
|
|
I18n.t('invite.not_found', base_url: Discourse.base_url)
|
|
end
|
|
|
|
render layout: 'no_ember'
|
|
end
|
|
end
|
|
|
|
def create
|
|
if params[:email].present? && Invite.exists?(email: params[:email])
|
|
return render json: failed_json, status: 422
|
|
end
|
|
|
|
if params[:topic_id].present?
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
|
|
guardian.ensure_can_invite_to!(topic)
|
|
end
|
|
|
|
if params[:group_ids].present? || params[:group_names].present?
|
|
groups = Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names])
|
|
end
|
|
|
|
guardian.ensure_can_invite_to_forum!(groups)
|
|
|
|
begin
|
|
invite = Invite.generate(current_user,
|
|
invite_key: params[:invite_key],
|
|
email: params[:email],
|
|
skip_email: params[:skip_email],
|
|
invited_by: current_user,
|
|
custom_message: params[:custom_message],
|
|
max_redemptions_allowed: params[:max_redemptions_allowed],
|
|
topic_id: topic&.id,
|
|
group_ids: groups&.map(&:id),
|
|
expires_at: params[:expires_at],
|
|
)
|
|
|
|
if invite.present?
|
|
render_serialized(invite, InviteSerializer, scope: guardian, root: nil, show_emails: params.has_key?(:email))
|
|
else
|
|
render json: failed_json, status: 422
|
|
end
|
|
rescue Invite::UserExists, ActiveRecord::RecordInvalid => e
|
|
render_json_error(e.message)
|
|
end
|
|
end
|
|
|
|
def update
|
|
invite = Invite.find_by(invited_by: current_user, id: params[:id])
|
|
raise Discourse::InvalidParameters.new(:id) if invite.blank?
|
|
|
|
if params[:topic_id].present?
|
|
topic = Topic.find_by(id: params[:topic_id])
|
|
raise Discourse::InvalidParameters.new(:topic_id) if topic.blank?
|
|
guardian.ensure_can_invite_to!(topic)
|
|
end
|
|
|
|
if params[:group_ids].present? || params[:group_names].present?
|
|
groups = Group.lookup_groups(group_ids: params[:group_ids], group_names: params[:group_names])
|
|
end
|
|
|
|
guardian.ensure_can_invite_to_forum!(groups)
|
|
|
|
Invite.transaction do
|
|
if params.has_key?(:topic_id)
|
|
invite.topic_invites.destroy_all
|
|
invite.topic_invites.create!(topic_id: topic.id) if topic.present?
|
|
end
|
|
|
|
if params.has_key?(:group_ids) || params.has_key?(:group_names)
|
|
invite.invited_groups.destroy_all
|
|
groups.each { |group| invite.invited_groups.find_or_create_by!(group_id: group.id) } if groups.present?
|
|
end
|
|
|
|
if params.has_key?(:email)
|
|
old_email = invite.email.presence
|
|
new_email = params[:email].presence
|
|
|
|
if old_email != new_email
|
|
invite.emailed_status = if new_email && !params[:skip_email]
|
|
Invite.emailed_status_types[:pending]
|
|
else
|
|
Invite.emailed_status_types[:not_required]
|
|
end
|
|
end
|
|
end
|
|
|
|
if params[:send_email]
|
|
if invite.emailed_status != Invite.emailed_status_types[:pending]
|
|
begin
|
|
RateLimiter.new(current_user, "resend-invite-per-hour", 10, 1.hour).performed!
|
|
rescue RateLimiter::LimitExceeded
|
|
return render_json_error(I18n.t("rate_limiter.slow_down"))
|
|
end
|
|
end
|
|
|
|
invite.emailed_status = Invite.emailed_status_types[:pending]
|
|
end
|
|
|
|
begin
|
|
invite.update!(params.permit(:email, :custom_message, :max_redemptions_allowed, :expires_at))
|
|
rescue ActiveRecord::RecordInvalid => e
|
|
return render_json_error(e.message)
|
|
end
|
|
end
|
|
|
|
if invite.emailed_status == Invite.emailed_status_types[:pending]
|
|
invite.update_column(:emailed_status, Invite.emailed_status_types[:sending])
|
|
Jobs.enqueue(:invite_email, invite_id: invite.id)
|
|
end
|
|
|
|
render_serialized(invite, InviteSerializer, scope: guardian, root: nil, show_emails: params.has_key?(:email))
|
|
end
|
|
|
|
def destroy
|
|
params.require(:id)
|
|
|
|
invite = Invite.find_by(invited_by_id: current_user.id, id: params[:id])
|
|
raise Discourse::InvalidParameters.new(:id) if invite.blank?
|
|
|
|
invite.trash!(current_user)
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def perform_accept_invitation
|
|
params.require(:id)
|
|
params.permit(:email, :username, :name, :password, :timezone, user_custom_fields: {})
|
|
|
|
invite = Invite.find_by(invite_key: params[:id])
|
|
|
|
if invite.present?
|
|
begin
|
|
attrs = {
|
|
username: params[:username],
|
|
name: params[:name],
|
|
password: params[:password],
|
|
user_custom_fields: params[:user_custom_fields],
|
|
ip_address: request.remote_ip
|
|
}
|
|
|
|
attrs[:email] =
|
|
if invite.is_invite_link?
|
|
params.require([:email])
|
|
params[:email]
|
|
else
|
|
invite.email
|
|
end
|
|
|
|
user = invite.redeem(attrs)
|
|
rescue ActiveRecord::RecordInvalid, ActiveRecord::RecordNotSaved => e
|
|
return render json: failed_json.merge(errors: e.record&.errors&.to_hash, message: I18n.t('invite.error_message')), status: 412
|
|
rescue Invite::UserExists => e
|
|
return render json: failed_json.merge(message: e.message), status: 412
|
|
end
|
|
|
|
if user.blank?
|
|
return render json: failed_json.merge(message: I18n.t('invite.not_found_json')), status: 404
|
|
end
|
|
|
|
log_on_user(user) if user.active?
|
|
user.update_timezone_if_missing(params[:timezone])
|
|
post_process_invite(user)
|
|
|
|
topic = invite.topics.first
|
|
response = {}
|
|
|
|
if user.present? && user.active?
|
|
response[:redirect_to] = topic.present? ? path(topic.relative_url) : path("/")
|
|
elsif user.present?
|
|
response[:message] = I18n.t('invite.confirm_email')
|
|
cookies[:destination_url] = path(topic.relative_url) if topic.present?
|
|
end
|
|
|
|
render json: success_json.merge(response)
|
|
else
|
|
render json: failed_json.merge(message: I18n.t('invite.not_found_json')), status: 404
|
|
end
|
|
end
|
|
|
|
def destroy_all_expired
|
|
guardian.ensure_can_destroy_all_invites!(current_user)
|
|
|
|
Invite
|
|
.where(invited_by: current_user)
|
|
.where('expires_at < ?', Time.zone.now)
|
|
.find_each { |invite| invite.trash!(current_user) }
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def resend_invite
|
|
params.require(:email)
|
|
RateLimiter.new(current_user, "resend-invite-per-hour", 10, 1.hour).performed!
|
|
|
|
invite = Invite.find_by(invited_by_id: current_user.id, email: params[:email])
|
|
raise Discourse::InvalidParameters.new(:email) if invite.blank?
|
|
invite.resend_invite
|
|
render json: success_json
|
|
|
|
rescue RateLimiter::LimitExceeded
|
|
render_json_error(I18n.t("rate_limiter.slow_down"))
|
|
end
|
|
|
|
def resend_all_invites
|
|
guardian.ensure_can_resend_all_invites!(current_user)
|
|
|
|
Invite
|
|
.left_outer_joins(:invited_users)
|
|
.where(invited_by: current_user)
|
|
.where('invites.email IS NOT NULL')
|
|
.where('invited_users.user_id IS NULL')
|
|
.group('invites.id')
|
|
.find_each { |invite| invite.resend_invite }
|
|
|
|
render json: success_json
|
|
end
|
|
|
|
def upload_csv
|
|
require 'csv'
|
|
|
|
guardian.ensure_can_bulk_invite_to_forum!(current_user)
|
|
|
|
hijack do
|
|
begin
|
|
file = params[:file] || params[:files].first
|
|
|
|
count = 0
|
|
invites = []
|
|
max_bulk_invites = SiteSetting.max_bulk_invites
|
|
CSV.foreach(file.tempfile) do |row|
|
|
count += 1
|
|
invites.push(email: row[0], groups: row[1], topic_id: row[2]) if row[0].present?
|
|
break if count >= max_bulk_invites
|
|
end
|
|
|
|
if invites.present?
|
|
Jobs.enqueue(:bulk_invite, invites: invites, current_user_id: current_user.id)
|
|
if count >= max_bulk_invites
|
|
render json: failed_json.merge(errors: [I18n.t("bulk_invite.max_rows", max_bulk_invites: max_bulk_invites)]), status: 422
|
|
else
|
|
render json: success_json
|
|
end
|
|
else
|
|
render json: failed_json.merge(errors: [I18n.t("bulk_invite.error")]), status: 422
|
|
end
|
|
rescue
|
|
render json: failed_json.merge(errors: [I18n.t("bulk_invite.error")]), status: 422
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def ensure_new_registrations_allowed
|
|
unless SiteSetting.allow_new_registrations
|
|
flash[:error] = I18n.t('login.new_registrations_disabled')
|
|
render layout: 'no_ember'
|
|
false
|
|
end
|
|
end
|
|
|
|
def ensure_not_logged_in
|
|
if current_user
|
|
flash[:error] = I18n.t("login.already_logged_in")
|
|
render layout: 'no_ember'
|
|
false
|
|
end
|
|
end
|
|
|
|
def post_process_invite(user)
|
|
user.enqueue_welcome_message('welcome_invite') if user.send_welcome_message
|
|
|
|
Group.refresh_automatic_groups!(:admins, :moderators, :staff) if user.staff?
|
|
|
|
if user.has_password?
|
|
send_activation_email(user) unless user.active
|
|
elsif !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins
|
|
Jobs.enqueue(:invite_password_instructions_email, username: user.username)
|
|
end
|
|
end
|
|
|
|
def send_activation_email(user)
|
|
email_token = user.email_tokens.create!(email: user.email)
|
|
|
|
Jobs.enqueue(:critical_user_email,
|
|
type: :signup,
|
|
user_id: user.id,
|
|
email_token: email_token.token
|
|
)
|
|
end
|
|
end
|