discourse/app/controllers/invites_controller.rb

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

342 lines
11 KiB
Ruby
Raw Normal View History

# frozen_string_literal: true
2013-02-05 14:16:51 -05:00
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_invites_allowed, only: [:show, :perform_accept_invitation]
before_action :ensure_new_registrations_allowed, only: [:show, :perform_accept_invitation]
before_action :ensure_not_logged_in, only: [:show, :perform_accept_invitation]
2013-02-05 14:16:51 -05:00
def show
expires_now
invite = Invite.find_by(invite_key: params[:id])
if invite.present? && !invite.expired? && !invite.redeemed?
email = Email.obfuscate(invite.email)
# Show email if the user already authenticated their email
if session[:authentication]
auth_result = Auth::Result.from_session_data(session[:authentication], user: nil)
if invite.email == auth_result.email
email = invite.email
end
end
store_preloaded("invite_info", MultiJson.dump(
invited_by: UserNameSerializer.new(invite.invited_by, scope: guardian, root: false),
email: 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
2013-02-05 14:16:51 -05:00
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,
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
2015-08-25 21:41:52 -04:00
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
2015-08-25 21:41:52 -04:00
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))
2015-08-25 21:41:52 -04:00
end
2013-02-05 14:16:51 -05:00
def destroy
params.require(:id)
2013-02-05 14:16:51 -05:00
invite = Invite.find_by(invited_by_id: current_user.id, id: params[:id])
raise Discourse::InvalidParameters.new(:id) if invite.blank?
2013-07-09 15:20:18 -04:00
invite.trash!(current_user)
2013-02-05 14:16:51 -05:00
render json: success_json
2013-02-05 14:16:51 -05:00
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,
session: session
}
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
2014-10-06 14:48:56 -04:00
def resend_invite
params.require(:email)
2016-06-06 15:36:59 -04:00
RateLimiter.new(current_user, "resend-invite-per-hour", 10, 1.hour).performed!
2014-10-06 14:48:56 -04:00
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
2016-06-06 15:36:59 -04:00
rescue RateLimiter::LimitExceeded
render_json_error(I18n.t("rate_limiter.slow_down"))
2014-10-06 14:48:56 -04:00
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
2016-12-04 11:06:35 -05:00
def upload_csv
require 'csv'
2014-05-27 16:14:37 -04:00
guardian.ensure_can_bulk_invite_to_forum!(current_user)
hijack do
begin
file = params[:file] || params[:files].first
2016-12-04 11:06:35 -05:00
2019-06-12 05:05:21 -04:00
count = 0
invites = []
2019-06-12 05:05:21 -04:00
max_bulk_invites = SiteSetting.max_bulk_invites
CSV.foreach(file.tempfile) do |row|
2019-06-12 05:05:21 -04:00
count += 1
2019-06-12 07:14:17 -04:00
invites.push(email: row[0], groups: row[1], topic_id: row[2]) if row[0].present?
2019-06-12 05:05:21 -04:00
break if count >= max_bulk_invites
end
2019-06-12 05:05:21 -04:00
if invites.present?
Jobs.enqueue(:bulk_invite, invites: invites, current_user_id: current_user.id)
2019-06-12 05:05:21 -04:00
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
2016-12-04 11:06:35 -05:00
end
2014-05-27 16:14:37 -04:00
end
end
private
2014-07-14 11:56:26 -04:00
def ensure_invites_allowed
if SiteSetting.enable_discourse_connect || (!SiteSetting.enable_local_logins && Discourse.enabled_auth_providers.count == 0)
raise Discourse::NotFound
end
end
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
FEATURE: Rename 'Discourse SSO' to DiscourseConnect (#11978) The 'Discourse SSO' protocol is being rebranded to DiscourseConnect. This should help to reduce confusion when 'SSO' is used in the generic sense. This commit aims to: - Rename `sso_` site settings. DiscourseConnect specific ones are prefixed `discourse_connect_`. Generic settings are prefixed `auth_` - Add (server-side-only) backwards compatibility for the old setting names, with deprecation notices - Copy `site_settings` database records to the new names - Rename relevant translation keys - Update relevant translations This commit does **not** aim to: - Rename any Ruby classes or methods. This might be done in a future commit - Change any URLs. This would break existing integrations - Make any changes to the protocol. This would break existing integrations - Change any functionality. Further normalization across DiscourseConnect and other auth methods will be done separately The risks are: - There is no backwards compatibility for site settings on the client-side. Accessing auth-related site settings in Javascript is fairly rare, and an error on the client side would not be security-critical. - If a plugin is monkey-patching parts of the auth process, changes to locale keys could cause broken error messages. This should also be unlikely. The old site setting names remain functional, so security-related overrides will remain working. A follow-up commit will be made with a post-deploy migration to delete the old `site_settings` rows.
2021-02-08 05:04:33 -05:00
elsif !SiteSetting.enable_discourse_connect && SiteSetting.enable_local_logins
Jobs.enqueue(:invite_password_instructions_email, username: user.username)
end
2018-06-07 01:28:18 -04:00
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
2013-02-05 14:16:51 -05:00
end