2019-05-02 17:17:27 -05:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
class Invite < ActiveRecord::Base
|
2017-05-02 04:43:33 -05:00
|
|
|
class UserExists < StandardError; end
|
2021-03-18 19:20:10 -05:00
|
|
|
class RedemptionFailed < StandardError; end
|
|
|
|
class ValidationFailed < StandardError; end
|
2021-03-03 03:45:29 -06:00
|
|
|
|
2015-01-19 12:50:01 -06:00
|
|
|
include RateLimiter::OnCreateRecord
|
2013-05-06 23:39:01 -05:00
|
|
|
include Trashable
|
2013-02-07 09:45:24 -06:00
|
|
|
|
2020-06-09 10:19:32 -05:00
|
|
|
# TODO(2021-05-22): remove
|
|
|
|
self.ignored_columns = %w{
|
|
|
|
user_id
|
|
|
|
redeemed_at
|
|
|
|
}
|
|
|
|
|
2019-07-19 00:59:12 -05:00
|
|
|
BULK_INVITE_EMAIL_LIMIT = 200
|
|
|
|
|
2015-01-19 12:50:01 -06:00
|
|
|
rate_limit :limit_invites_per_day
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
belongs_to :user
|
|
|
|
belongs_to :topic
|
2013-02-28 12:54:12 -06:00
|
|
|
belongs_to :invited_by, class_name: 'User'
|
2013-02-05 13:16:51 -06:00
|
|
|
|
2020-06-09 10:19:32 -05:00
|
|
|
has_many :invited_users
|
|
|
|
has_many :users, through: :invited_users
|
2014-05-08 01:45:49 -05:00
|
|
|
has_many :invited_groups
|
|
|
|
has_many :groups, through: :invited_groups
|
2013-02-05 13:16:51 -06:00
|
|
|
has_many :topic_invites
|
2013-02-07 09:45:24 -06:00
|
|
|
has_many :topics, through: :topic_invites, source: :topic
|
2021-03-03 03:45:29 -06:00
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
validates_presence_of :invited_by_id
|
2020-06-02 21:13:25 -05:00
|
|
|
validates :email, email: true, allow_blank: true
|
2021-03-03 03:45:29 -06:00
|
|
|
validate :ensure_max_redemptions_allowed
|
|
|
|
validate :user_doesnt_already_exist
|
2013-02-05 13:16:51 -06:00
|
|
|
|
|
|
|
before_create do
|
|
|
|
self.invite_key ||= SecureRandom.hex
|
2020-06-09 10:19:32 -05:00
|
|
|
self.expires_at ||= SiteSetting.invite_expiry_days.days.from_now
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
2014-07-14 10:56:26 -05:00
|
|
|
before_validation do
|
|
|
|
self.email = Email.downcase(email) unless email.nil?
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
attr_accessor :email_already_exists
|
2013-02-07 09:45:24 -06:00
|
|
|
|
2019-07-19 00:59:12 -05:00
|
|
|
def self.emailed_status_types
|
|
|
|
@emailed_status_types ||= Enum.new(not_required: 0, pending: 1, bulk_pending: 2, sending: 3, sent: 4)
|
|
|
|
end
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
def user_doesnt_already_exist
|
|
|
|
@email_already_exists = false
|
|
|
|
return if email.blank?
|
2018-01-19 08:29:15 -06:00
|
|
|
user = Invite.find_user_by_email(email)
|
2017-04-26 13:47:36 -05:00
|
|
|
|
2020-06-09 10:19:32 -05:00
|
|
|
if user && user.id != self.invited_users&.first&.user_id
|
2013-02-05 13:16:51 -06:00
|
|
|
@email_already_exists = true
|
2021-03-06 05:29:35 -06:00
|
|
|
errors.add(:email, I18n.t(
|
|
|
|
"invite.user_exists",
|
|
|
|
email: email,
|
|
|
|
username: user.username,
|
|
|
|
base_path: Discourse.base_path
|
|
|
|
))
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2020-06-09 10:19:32 -05:00
|
|
|
def is_invite_link?
|
2021-01-20 02:50:02 -06:00
|
|
|
email.blank?
|
2020-06-09 10:19:32 -05:00
|
|
|
end
|
|
|
|
|
2021-03-18 19:20:10 -05:00
|
|
|
def redeemable?
|
2021-03-25 11:26:22 -05:00
|
|
|
!redeemed? && !expired? && !deleted_at? && !destroyed? && link_valid?
|
2021-03-18 19:20:10 -05:00
|
|
|
end
|
|
|
|
|
2013-02-05 13:16:51 -06:00
|
|
|
def redeemed?
|
2020-06-09 10:19:32 -05:00
|
|
|
if is_invite_link?
|
|
|
|
redemption_count >= max_redemptions_allowed
|
|
|
|
else
|
|
|
|
self.invited_users.count > 0
|
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
|
|
|
def expired?
|
2020-06-09 10:19:32 -05:00
|
|
|
expires_at < Time.zone.now
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
def link
|
|
|
|
"#{Discourse.base_url}/invites/#{invite_key}"
|
2015-08-31 09:06:13 -05:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
def link_valid?
|
|
|
|
invalidated_at.nil?
|
2015-08-31 09:06:13 -05:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
def self.generate(invited_by, opts = nil)
|
2016-09-20 12:12:00 -05:00
|
|
|
opts ||= {}
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
email = Email.downcase(opts[:email]) if opts[:email].present?
|
2014-05-08 20:45:18 -05:00
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
if user = find_user_by_email(email)
|
|
|
|
raise UserExists.new(I18n.t(
|
|
|
|
"invite.user_exists",
|
|
|
|
email: email,
|
2018-12-05 09:43:07 -06:00
|
|
|
username: user.username,
|
|
|
|
base_path: Discourse.base_path
|
|
|
|
))
|
2014-05-08 20:45:18 -05:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
if email.present?
|
|
|
|
invite = Invite
|
|
|
|
.with_deleted
|
|
|
|
.where(email: email, invited_by_id: invited_by.id)
|
|
|
|
.order('created_at DESC')
|
|
|
|
.first
|
2014-01-21 14:13:55 -06:00
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
if invite && (invite.expired? || invite.deleted_at)
|
|
|
|
invite.destroy
|
|
|
|
invite = nil
|
|
|
|
end
|
2014-01-21 14:13:55 -06:00
|
|
|
end
|
2013-11-06 11:56:26 -06:00
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
emailed_status = if opts[:skip_email] || invite&.emailed_status == emailed_status_types[:not_required]
|
|
|
|
emailed_status_types[:not_required]
|
|
|
|
elsif opts[:emailed_status].present?
|
|
|
|
opts[:emailed_status]
|
|
|
|
elsif email.present?
|
|
|
|
emailed_status_types[:pending]
|
|
|
|
else
|
|
|
|
emailed_status_types[:not_required]
|
|
|
|
end
|
2019-07-19 00:59:12 -05:00
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
if invite
|
2018-12-10 16:24:02 -06:00
|
|
|
invite.update_columns(
|
|
|
|
created_at: Time.zone.now,
|
|
|
|
updated_at: Time.zone.now,
|
2021-03-03 03:45:29 -06:00
|
|
|
expires_at: opts[:expires_at] || SiteSetting.invite_expiry_days.days.from_now,
|
2019-07-19 00:59:12 -05:00
|
|
|
emailed_status: emailed_status
|
2018-12-10 16:24:02 -06:00
|
|
|
)
|
|
|
|
else
|
2021-03-11 10:19:32 -06:00
|
|
|
create_args = opts.slice(:email, :moderator, :custom_message, :max_redemptions_allowed)
|
2021-03-03 03:45:29 -06:00
|
|
|
create_args[:invited_by] = invited_by
|
|
|
|
create_args[:email] = email
|
|
|
|
create_args[:emailed_status] = emailed_status
|
|
|
|
create_args[:expires_at] = opts[:expires_at] || SiteSetting.invite_expiry_days.days.from_now
|
2017-04-11 09:05:35 -05:00
|
|
|
|
2016-09-20 12:12:00 -05:00
|
|
|
invite = Invite.create!(create_args)
|
2013-11-06 11:56:26 -06:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
topic_id = opts[:topic]&.id || opts[:topic_id]
|
|
|
|
if topic_id.present?
|
|
|
|
invite.topic_invites.find_or_create_by!(topic_id: topic_id)
|
2014-05-08 20:45:18 -05:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
group_ids = opts[:group_ids]
|
2014-05-08 20:45:18 -05:00
|
|
|
if group_ids.present?
|
|
|
|
group_ids.each do |group_id|
|
2021-03-03 03:45:29 -06:00
|
|
|
invite.invited_groups.find_or_create_by!(group_id: group_id)
|
2014-05-08 20:45:18 -05:00
|
|
|
end
|
|
|
|
end
|
2013-11-06 11:56:26 -06:00
|
|
|
|
2019-07-19 00:59:12 -05:00
|
|
|
if emailed_status == emailed_status_types[:pending]
|
2021-03-03 03:45:29 -06:00
|
|
|
invite.update_column(:emailed_status, emailed_status_types[:sending])
|
2021-03-16 10:08:54 -05:00
|
|
|
Jobs.enqueue(:invite_email, invite_id: invite.id, invite_to_topic: opts[:invite_to_topic])
|
2019-07-19 00:59:12 -05:00
|
|
|
end
|
2014-05-08 20:45:18 -05:00
|
|
|
|
|
|
|
invite.reload
|
2013-11-06 11:56:26 -06:00
|
|
|
end
|
|
|
|
|
2021-03-02 01:13:04 -06:00
|
|
|
def redeem(email: nil, username: nil, name: nil, password: nil, user_custom_fields: nil, ip_address: nil, session: nil)
|
2021-03-18 19:20:10 -05:00
|
|
|
return if !redeemable?
|
|
|
|
|
|
|
|
if is_invite_link? && UserEmail.exists?(email: email)
|
|
|
|
raise UserExists.new I18n.t("invite_link.email_taken")
|
2020-06-09 10:19:32 -05:00
|
|
|
end
|
2021-03-18 19:20:10 -05:00
|
|
|
|
|
|
|
email = self.email if email.blank? && !is_invite_link?
|
|
|
|
InviteRedeemer.new(
|
|
|
|
invite: self,
|
|
|
|
email: email,
|
|
|
|
username: username,
|
|
|
|
name: name,
|
|
|
|
password: password,
|
|
|
|
user_custom_fields: user_custom_fields,
|
|
|
|
ip_address: ip_address,
|
|
|
|
session: session
|
|
|
|
).redeem
|
2020-06-09 10:19:32 -05:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
def self.redeem_from_email(email)
|
|
|
|
invite = Invite.find_by(email: Email.downcase(email))
|
|
|
|
InviteRedeemer.new(invite: invite, email: invite.email).redeem if invite
|
|
|
|
invite
|
2020-06-09 10:19:32 -05:00
|
|
|
end
|
|
|
|
|
2018-01-19 08:29:15 -06:00
|
|
|
def self.find_user_by_email(email)
|
2019-10-30 01:08:47 -05:00
|
|
|
User.with_email(Email.downcase(email)).where(staged: false).first
|
2018-01-19 08:29:15 -06:00
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
def self.pending(inviter)
|
|
|
|
Invite.distinct
|
2020-06-09 10:19:32 -05:00
|
|
|
.joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id")
|
|
|
|
.joins("LEFT JOIN users ON invited_users.user_id = users.id")
|
|
|
|
.where(invited_by_id: inviter.id)
|
2021-03-03 03:45:29 -06:00
|
|
|
.where('redemption_count < max_redemptions_allowed')
|
2021-03-06 05:29:35 -06:00
|
|
|
.where('expires_at > ?', Time.zone.now)
|
2020-06-09 10:19:32 -05:00
|
|
|
.order('invites.updated_at DESC')
|
2013-11-08 13:11:41 -06:00
|
|
|
end
|
|
|
|
|
2021-03-06 05:29:35 -06:00
|
|
|
def self.expired(inviter)
|
|
|
|
Invite.distinct
|
|
|
|
.joins("LEFT JOIN invited_users ON invites.id = invited_users.invite_id")
|
|
|
|
.joins("LEFT JOIN users ON invited_users.user_id = users.id")
|
|
|
|
.where(invited_by_id: inviter.id)
|
2021-03-23 11:57:39 -05:00
|
|
|
.where('redemption_count < max_redemptions_allowed')
|
|
|
|
.where('expires_at < ?', Time.zone.now)
|
2021-03-06 05:29:35 -06:00
|
|
|
.order('invites.expires_at ASC')
|
|
|
|
end
|
|
|
|
|
2021-03-03 03:45:29 -06:00
|
|
|
def self.redeemed_users(inviter)
|
|
|
|
InvitedUser
|
2021-03-06 05:29:35 -06:00
|
|
|
.joins("LEFT JOIN invites ON invites.id = invited_users.invite_id")
|
2020-06-09 10:19:32 -05:00
|
|
|
.includes(user: :user_stat)
|
|
|
|
.where('invited_users.user_id IS NOT NULL')
|
|
|
|
.where('invites.invited_by_id = ?', inviter.id)
|
2021-03-06 05:29:35 -06:00
|
|
|
.order('invited_users.redeemed_at DESC')
|
2020-06-09 10:19:32 -05:00
|
|
|
.references('invite')
|
|
|
|
.references('user')
|
|
|
|
.references('user_stat')
|
|
|
|
end
|
|
|
|
|
2014-01-21 15:53:46 -06:00
|
|
|
def self.invalidate_for_email(email)
|
2014-05-06 08:41:59 -05:00
|
|
|
i = Invite.find_by(email: Email.downcase(email))
|
2014-01-21 15:53:46 -06:00
|
|
|
if i
|
|
|
|
i.invalidated_at = Time.zone.now
|
|
|
|
i.save
|
|
|
|
end
|
|
|
|
i
|
|
|
|
end
|
2014-05-27 15:14:37 -05:00
|
|
|
|
2014-10-06 13:48:56 -05:00
|
|
|
def resend_invite
|
2020-10-26 05:26:43 -05:00
|
|
|
self.update_columns(updated_at: Time.zone.now, invalidated_at: nil, expires_at: SiteSetting.invite_expiry_days.days.from_now)
|
2014-10-06 13:48:56 -05:00
|
|
|
Jobs.enqueue(:invite_email, invite_id: self.id)
|
|
|
|
end
|
|
|
|
|
2015-01-19 12:50:01 -06:00
|
|
|
def limit_invites_per_day
|
2015-02-11 00:45:46 -06:00
|
|
|
RateLimiter.new(invited_by, "invites-per-day", SiteSetting.max_invites_per_day, 1.day.to_i)
|
2015-01-19 12:50:01 -06:00
|
|
|
end
|
|
|
|
|
2014-05-27 15:14:37 -05:00
|
|
|
def self.base_directory
|
2014-11-25 10:55:09 -06:00
|
|
|
File.join(Rails.root, "public", "uploads", "csv", RailsMultisite::ConnectionManagement.current_db)
|
2014-05-27 15:14:37 -05:00
|
|
|
end
|
2020-06-09 10:19:32 -05:00
|
|
|
|
|
|
|
def ensure_max_redemptions_allowed
|
2021-03-03 03:45:29 -06:00
|
|
|
if self.max_redemptions_allowed.nil?
|
|
|
|
self.max_redemptions_allowed = 1
|
2021-03-06 05:29:35 -06:00
|
|
|
else
|
|
|
|
limit = invited_by&.staff? ? SiteSetting.invite_link_max_redemptions_limit
|
|
|
|
: SiteSetting.invite_link_max_redemptions_limit_users
|
|
|
|
|
|
|
|
if !self.max_redemptions_allowed.between?(1, limit)
|
|
|
|
errors.add(:max_redemptions_allowed, I18n.t("invite_link.max_redemptions_limit", max_limit: limit))
|
|
|
|
end
|
2020-06-09 10:19:32 -05:00
|
|
|
end
|
|
|
|
end
|
2013-02-05 13:16:51 -06:00
|
|
|
end
|
2013-05-23 21:48:32 -05:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: invites
|
|
|
|
#
|
2020-06-09 10:19:32 -05:00
|
|
|
# id :integer not null, primary key
|
|
|
|
# invite_key :string(32) not null
|
|
|
|
# email :string
|
|
|
|
# invited_by_id :integer not null
|
|
|
|
# created_at :datetime not null
|
|
|
|
# updated_at :datetime not null
|
|
|
|
# deleted_at :datetime
|
|
|
|
# deleted_by_id :integer
|
|
|
|
# invalidated_at :datetime
|
|
|
|
# moderator :boolean default(FALSE), not null
|
|
|
|
# custom_message :text
|
|
|
|
# emailed_status :integer
|
|
|
|
# max_redemptions_allowed :integer default(1), not null
|
|
|
|
# redemption_count :integer default(0), not null
|
|
|
|
# expires_at :datetime not null
|
2013-05-23 21:48:32 -05:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2014-07-29 12:57:08 -05:00
|
|
|
# index_invites_on_email_and_invited_by_id (email,invited_by_id)
|
2019-07-19 00:59:12 -05:00
|
|
|
# index_invites_on_emailed_status (emailed_status)
|
2013-05-23 21:48:32 -05:00
|
|
|
# index_invites_on_invite_key (invite_key) UNIQUE
|
2019-12-30 00:13:27 -06:00
|
|
|
# index_invites_on_invited_by_id (invited_by_id)
|
2013-05-23 21:48:32 -05:00
|
|
|
#
|