2019-05-02 18:17:27 -04:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
class Invite < ActiveRecord::Base
|
2017-05-02 05:43:33 -04:00
|
|
|
class UserExists < StandardError
|
|
|
|
end
|
2021-03-18 20:20:10 -04:00
|
|
|
class RedemptionFailed < StandardError
|
|
|
|
end
|
|
|
|
class ValidationFailed < StandardError
|
|
|
|
end
|
2021-03-03 04:45:29 -05:00
|
|
|
|
2015-01-19 13:50:01 -05:00
|
|
|
include RateLimiter::OnCreateRecord
|
2013-05-07 00:39:01 -04:00
|
|
|
include Trashable
|
2013-02-07 10:45:24 -05:00
|
|
|
|
2022-01-10 21:57:21 -05:00
|
|
|
# TODO(2021-05-22): remove
|
|
|
|
self.ignored_columns = %w[user_id redeemed_at]
|
|
|
|
|
2019-07-19 01:59:12 -04:00
|
|
|
BULK_INVITE_EMAIL_LIMIT = 200
|
2021-12-13 00:39:14 -05:00
|
|
|
DOMAIN_REGEX =
|
|
|
|
/\A(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)+([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])\z/
|
2019-07-19 01:59:12 -04:00
|
|
|
|
2015-01-19 13:50:01 -05:00
|
|
|
rate_limit :limit_invites_per_day
|
|
|
|
|
2013-02-28 13:54:12 -05:00
|
|
|
belongs_to :invited_by, class_name: "User"
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2020-06-09 11:19:32 -04:00
|
|
|
has_many :invited_users
|
|
|
|
has_many :users, through: :invited_users
|
2014-05-08 02:45:49 -04:00
|
|
|
has_many :invited_groups
|
|
|
|
has_many :groups, through: :invited_groups
|
2013-02-05 14:16:51 -05:00
|
|
|
has_many :topic_invites
|
2013-02-07 10:45:24 -05:00
|
|
|
has_many :topics, through: :topic_invites, source: :topic
|
2021-03-03 04:45:29 -05:00
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
validates_presence_of :invited_by_id
|
2020-06-02 22:13:25 -04:00
|
|
|
validates :email, email: true, allow_blank: true
|
2023-05-11 06:09:26 -04:00
|
|
|
validates :custom_message, length: { maximum: 1000 }
|
2021-03-03 04:45:29 -05:00
|
|
|
validate :ensure_max_redemptions_allowed
|
2022-09-30 06:35:00 -04:00
|
|
|
validate :valid_redemption_count
|
2021-12-08 10:06:57 -05:00
|
|
|
validate :valid_domain, if: :will_save_change_to_domain?
|
2022-01-20 05:54:38 -05:00
|
|
|
validate :user_doesnt_already_exist, if: :will_save_change_to_email?
|
2022-09-30 06:35:00 -04:00
|
|
|
validate :email_xor_domain
|
2013-02-05 14:16:51 -05:00
|
|
|
|
|
|
|
before_create do
|
2021-04-14 12:22:16 -04:00
|
|
|
self.invite_key ||= SecureRandom.base58(10)
|
2020-06-09 11:19:32 -04:00
|
|
|
self.expires_at ||= SiteSetting.invite_expiry_days.days.from_now
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2021-04-14 05:15:56 -04:00
|
|
|
before_save do
|
|
|
|
self.email_token = email.present? ? SecureRandom.hex : nil if will_save_change_to_email?
|
|
|
|
end
|
|
|
|
|
2014-07-14 11:56:26 -04:00
|
|
|
before_validation { self.email = Email.downcase(email) unless email.nil? }
|
2013-02-05 14:16:51 -05:00
|
|
|
|
2022-05-10 11:45:43 -04:00
|
|
|
attribute :email_already_exists
|
2013-02-07 10:45:24 -05:00
|
|
|
|
2019-07-19 01:59:12 -04: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 14:16:51 -05:00
|
|
|
def user_doesnt_already_exist
|
2022-05-10 11:45:43 -04:00
|
|
|
self.email_already_exists = false
|
2013-02-05 14:16:51 -05:00
|
|
|
return if email.blank?
|
2018-01-19 09:29:15 -05:00
|
|
|
user = Invite.find_user_by_email(email)
|
2017-04-26 14:47:36 -04:00
|
|
|
|
2020-06-09 11:19:32 -04:00
|
|
|
if user && user.id != self.invited_users&.first&.user_id
|
2022-05-10 11:45:43 -04:00
|
|
|
self.email_already_exists = true
|
|
|
|
errors.add(:base, user_exists_error_msg(email))
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2022-09-30 06:35:00 -04:00
|
|
|
def email_xor_domain
|
|
|
|
errors.add(:base, I18n.t("invite.email_xor_domain")) if email.present? && domain.present?
|
|
|
|
end
|
|
|
|
|
2022-11-13 21:02:06 -05:00
|
|
|
# Even if a domain is specified on the invite, it still counts as
|
|
|
|
# an invite link.
|
2020-06-09 11:19:32 -04:00
|
|
|
def is_invite_link?
|
2022-11-13 21:02:06 -05:00
|
|
|
self.email.blank?
|
|
|
|
end
|
|
|
|
|
|
|
|
# Email invites have specific behaviour and it's easier to visually
|
|
|
|
# parse is_email_invite? than !is_invite_link?
|
|
|
|
def is_email_invite?
|
|
|
|
self.email.present?
|
2020-06-09 11:19:32 -04:00
|
|
|
end
|
|
|
|
|
2021-03-18 20:20:10 -04:00
|
|
|
def redeemable?
|
2021-03-25 12:26:22 -04:00
|
|
|
!redeemed? && !expired? && !deleted_at? && !destroyed? && link_valid?
|
2021-03-18 20:20:10 -04:00
|
|
|
end
|
|
|
|
|
2022-11-17 00:51:58 -05:00
|
|
|
def redeemed_by_user?(redeeming_user)
|
|
|
|
self.invited_users.exists?(user: redeeming_user)
|
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
def redeemed?
|
2020-06-09 11:19:32 -04:00
|
|
|
if is_invite_link?
|
|
|
|
redemption_count >= max_redemptions_allowed
|
|
|
|
else
|
|
|
|
self.invited_users.count > 0
|
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2022-11-01 12:33:32 -04:00
|
|
|
def email_matches?(email)
|
|
|
|
email.downcase == self.email.downcase
|
|
|
|
end
|
|
|
|
|
|
|
|
def domain_matches?(email)
|
|
|
|
_, domain = email.split("@")
|
|
|
|
self.domain == domain
|
|
|
|
end
|
|
|
|
|
|
|
|
def can_be_redeemed_by?(user)
|
|
|
|
return false if !self.redeemable?
|
2022-11-17 00:51:58 -05:00
|
|
|
return false if redeemed_by_user?(user)
|
2022-11-24 20:57:04 -05:00
|
|
|
return true if self.domain.blank? && self.email.blank?
|
2022-11-01 12:33:32 -04:00
|
|
|
return true if self.email.present? && email_matches?(user.email)
|
|
|
|
self.domain.present? && domain_matches?(user.email)
|
|
|
|
end
|
|
|
|
|
2013-02-05 14:16:51 -05:00
|
|
|
def expired?
|
2020-06-09 11:19:32 -04:00
|
|
|
expires_at < Time.zone.now
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
|
|
|
|
2021-04-14 05:15:56 -04:00
|
|
|
def link(with_email_token: false)
|
|
|
|
if with_email_token
|
|
|
|
"#{Discourse.base_url}/invites/#{invite_key}?t=#{email_token}"
|
2023-01-09 07:20:10 -05:00
|
|
|
else
|
2021-04-14 05:15:56 -04:00
|
|
|
"#{Discourse.base_url}/invites/#{invite_key}"
|
2023-01-09 07:20:10 -05:00
|
|
|
end
|
2015-08-31 10:06:13 -04:00
|
|
|
end
|
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
def link_valid?
|
|
|
|
invalidated_at.nil?
|
2015-08-31 10:06:13 -04:00
|
|
|
end
|
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
def self.generate(invited_by, opts = nil)
|
2016-09-20 13:12:00 -04:00
|
|
|
opts ||= {}
|
2023-08-18 12:33:40 -04:00
|
|
|
time_zone = Time.find_zone(invited_by&.user_option&.timezone) || Time.zone
|
2021-03-03 04:45:29 -05:00
|
|
|
email = Email.downcase(opts[:email]) if opts[:email].present?
|
2014-05-08 21:45:18 -04:00
|
|
|
|
2022-05-10 11:45:43 -04:00
|
|
|
raise UserExists.new(new.user_exists_error_msg(email)) if find_user_by_email(email)
|
2014-05-08 21:45:18 -04:00
|
|
|
|
2021-03-03 04:45:29 -05: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 15:13:55 -05:00
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
if invite && (invite.expired? || invite.deleted_at)
|
|
|
|
invite.destroy
|
|
|
|
invite = nil
|
|
|
|
end
|
2022-08-10 01:39:26 -04:00
|
|
|
email_digest = Digest::SHA256.hexdigest(email)
|
|
|
|
RateLimiter.new(invited_by, "reinvites-per-day-#{email_digest}", 3, 1.day.to_i).performed!
|
2014-01-21 15:13:55 -05:00
|
|
|
end
|
2013-11-06 12:56:26 -05:00
|
|
|
|
2021-03-03 04:45:29 -05: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 01:59:12 -04:00
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
if invite
|
2018-12-10 17:24:02 -05:00
|
|
|
invite.update_columns(
|
|
|
|
created_at: Time.zone.now,
|
|
|
|
updated_at: Time.zone.now,
|
2023-08-18 12:33:40 -04:00
|
|
|
expires_at: opts[:expires_at] || time_zone.now + SiteSetting.invite_expiry_days.days,
|
2019-07-19 01:59:12 -04:00
|
|
|
emailed_status: emailed_status,
|
2018-12-10 17:24:02 -05:00
|
|
|
)
|
|
|
|
else
|
2021-12-08 10:06:57 -05:00
|
|
|
create_args =
|
|
|
|
opts.slice(:email, :domain, :moderator, :custom_message, :max_redemptions_allowed)
|
2021-03-03 04:45:29 -05:00
|
|
|
create_args[:invited_by] = invited_by
|
|
|
|
create_args[:email] = email
|
|
|
|
create_args[:emailed_status] = emailed_status
|
2023-08-18 12:33:40 -04:00
|
|
|
create_args[:expires_at] = opts[:expires_at] ||
|
|
|
|
time_zone.now + SiteSetting.invite_expiry_days.days
|
2017-04-11 10:05:35 -04:00
|
|
|
|
2016-09-20 13:12:00 -04:00
|
|
|
invite = Invite.create!(create_args)
|
2013-11-06 12:56:26 -05:00
|
|
|
end
|
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
topic_id = opts[:topic]&.id || opts[:topic_id]
|
|
|
|
invite.topic_invites.find_or_create_by!(topic_id: topic_id) if topic_id.present?
|
2014-05-08 21:45:18 -04:00
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
group_ids = opts[:group_ids]
|
2014-05-08 21:45:18 -04:00
|
|
|
if group_ids.present?
|
2021-03-03 04:45:29 -05:00
|
|
|
group_ids.each { |group_id| invite.invited_groups.find_or_create_by!(group_id: group_id) }
|
2014-05-08 21:45:18 -04:00
|
|
|
end
|
2013-11-06 12:56:26 -05:00
|
|
|
|
2019-07-19 01:59:12 -04:00
|
|
|
if emailed_status == emailed_status_types[:pending]
|
2021-03-03 04:45:29 -05:00
|
|
|
invite.update_column(:emailed_status, emailed_status_types[:sending])
|
2021-03-16 11:08:54 -04:00
|
|
|
Jobs.enqueue(:invite_email, invite_id: invite.id, invite_to_topic: opts[:invite_to_topic])
|
2019-07-19 01:59:12 -04:00
|
|
|
end
|
2014-05-08 21:45:18 -04:00
|
|
|
|
|
|
|
invite.reload
|
2013-11-06 12:56:26 -05:00
|
|
|
end
|
|
|
|
|
2022-11-01 12:33:32 -04:00
|
|
|
def redeem(
|
|
|
|
email: nil,
|
|
|
|
username: nil,
|
|
|
|
name: nil,
|
|
|
|
password: nil,
|
|
|
|
user_custom_fields: nil,
|
|
|
|
ip_address: nil,
|
|
|
|
session: nil,
|
|
|
|
email_token: nil,
|
|
|
|
redeeming_user: nil
|
|
|
|
)
|
2021-03-18 20:20:10 -04:00
|
|
|
return if !redeemable?
|
|
|
|
|
|
|
|
InviteRedeemer.new(
|
|
|
|
invite: self,
|
|
|
|
email: email,
|
|
|
|
username: username,
|
|
|
|
name: name,
|
|
|
|
password: password,
|
|
|
|
user_custom_fields: user_custom_fields,
|
|
|
|
ip_address: ip_address,
|
2021-04-14 05:15:56 -04:00
|
|
|
session: session,
|
2022-11-01 12:33:32 -04:00
|
|
|
email_token: email_token,
|
|
|
|
redeeming_user: redeeming_user,
|
2021-03-18 20:20:10 -04:00
|
|
|
).redeem
|
2020-06-09 11:19:32 -04:00
|
|
|
end
|
|
|
|
|
2022-11-01 12:33:32 -04:00
|
|
|
def self.redeem_for_existing_user(user)
|
|
|
|
invite = Invite.find_by(email: Email.downcase(user.email))
|
2022-08-04 22:20:48 -04:00
|
|
|
if invite.present? && invite.redeemable?
|
2022-11-01 12:33:32 -04:00
|
|
|
InviteRedeemer.new(invite: invite, redeeming_user: user).redeem
|
2022-08-04 22:20:48 -04:00
|
|
|
end
|
2021-03-03 04:45:29 -05:00
|
|
|
invite
|
2020-06-09 11:19:32 -04:00
|
|
|
end
|
|
|
|
|
2018-01-19 09:29:15 -05:00
|
|
|
def self.find_user_by_email(email)
|
2019-10-30 02:08:47 -04:00
|
|
|
User.with_email(Email.downcase(email)).where(staged: false).first
|
2018-01-19 09:29:15 -05:00
|
|
|
end
|
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
def self.pending(inviter)
|
|
|
|
Invite
|
|
|
|
.distinct
|
2020-06-09 11:19:32 -04: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 04:45:29 -05:00
|
|
|
.where("redemption_count < max_redemptions_allowed")
|
2021-03-06 06:29:35 -05:00
|
|
|
.where("expires_at > ?", Time.zone.now)
|
2020-06-09 11:19:32 -04:00
|
|
|
.order("invites.updated_at DESC")
|
2013-11-08 14:11:41 -05:00
|
|
|
end
|
|
|
|
|
2021-03-06 06:29:35 -05: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 12:57:39 -04:00
|
|
|
.where("redemption_count < max_redemptions_allowed")
|
|
|
|
.where("expires_at < ?", Time.zone.now)
|
2021-03-06 06:29:35 -05:00
|
|
|
.order("invites.expires_at ASC")
|
|
|
|
end
|
|
|
|
|
2021-03-03 04:45:29 -05:00
|
|
|
def self.redeemed_users(inviter)
|
|
|
|
InvitedUser
|
2021-03-06 06:29:35 -05:00
|
|
|
.joins("LEFT JOIN invites ON invites.id = invited_users.invite_id")
|
2020-06-09 11:19:32 -04:00
|
|
|
.includes(user: :user_stat)
|
|
|
|
.where("invited_users.user_id IS NOT NULL")
|
|
|
|
.where("invites.invited_by_id = ?", inviter.id)
|
2021-03-06 06:29:35 -05:00
|
|
|
.order("invited_users.redeemed_at DESC")
|
2020-06-09 11:19:32 -04:00
|
|
|
.references("invite")
|
|
|
|
.references("user")
|
|
|
|
.references("user_stat")
|
|
|
|
end
|
|
|
|
|
2014-01-21 16:53:46 -05:00
|
|
|
def self.invalidate_for_email(email)
|
2023-05-24 05:08:56 -04:00
|
|
|
Invite.find_by(email: Email.downcase(email))&.invalidate!
|
|
|
|
end
|
2021-12-08 10:06:57 -05:00
|
|
|
|
2023-05-24 05:08:56 -04:00
|
|
|
def invalidate!
|
|
|
|
update_attribute(:invalidated_at, Time.current)
|
|
|
|
self
|
2014-01-21 16:53:46 -05:00
|
|
|
end
|
2014-05-27 16:14:37 -04:00
|
|
|
|
2014-10-06 14:48:56 -04:00
|
|
|
def resend_invite
|
2020-10-26 06:26:43 -04:00
|
|
|
self.update_columns(
|
|
|
|
updated_at: Time.zone.now,
|
|
|
|
invalidated_at: nil,
|
|
|
|
expires_at: SiteSetting.invite_expiry_days.days.from_now,
|
|
|
|
)
|
2014-10-06 14:48:56 -04:00
|
|
|
Jobs.enqueue(:invite_email, invite_id: self.id)
|
|
|
|
end
|
|
|
|
|
2015-01-19 13:50:01 -05:00
|
|
|
def limit_invites_per_day
|
2015-02-11 01:45:46 -05:00
|
|
|
RateLimiter.new(invited_by, "invites-per-day", SiteSetting.max_invites_per_day, 1.day.to_i)
|
2015-01-19 13:50:01 -05:00
|
|
|
end
|
|
|
|
|
2014-05-27 16:14:37 -04:00
|
|
|
def self.base_directory
|
2014-11-25 11:55:09 -05:00
|
|
|
File.join(
|
|
|
|
Rails.root,
|
|
|
|
"public",
|
|
|
|
"uploads",
|
|
|
|
"csv",
|
|
|
|
RailsMultisite::ConnectionManagement.current_db,
|
|
|
|
)
|
2014-05-27 16:14:37 -04:00
|
|
|
end
|
2020-06-09 11:19:32 -04:00
|
|
|
|
|
|
|
def ensure_max_redemptions_allowed
|
2021-03-03 04:45:29 -05:00
|
|
|
if self.max_redemptions_allowed.nil?
|
|
|
|
self.max_redemptions_allowed = 1
|
2021-03-06 06:29:35 -05:00
|
|
|
else
|
|
|
|
limit =
|
2023-01-09 07:20:10 -05:00
|
|
|
(
|
2021-03-06 06:29:35 -05:00
|
|
|
if invited_by&.staff?
|
|
|
|
SiteSetting.invite_link_max_redemptions_limit
|
2023-01-09 07:20:10 -05:00
|
|
|
else
|
2021-03-06 06:29:35 -05:00
|
|
|
SiteSetting.invite_link_max_redemptions_limit_users
|
2023-01-09 07:20:10 -05:00
|
|
|
end
|
|
|
|
)
|
2021-03-06 06:29:35 -05:00
|
|
|
|
2022-09-30 06:35:00 -04:00
|
|
|
if self.email.present? && self.max_redemptions_allowed != 1
|
|
|
|
errors.add(:max_redemptions_allowed, I18n.t("invite.max_redemptions_allowed_one"))
|
|
|
|
elsif !self.max_redemptions_allowed.between?(1, limit)
|
2021-03-06 06:29:35 -05:00
|
|
|
errors.add(
|
|
|
|
:max_redemptions_allowed,
|
|
|
|
I18n.t("invite_link.max_redemptions_limit", max_limit: limit),
|
|
|
|
)
|
|
|
|
end
|
2020-06-09 11:19:32 -04:00
|
|
|
end
|
|
|
|
end
|
2021-12-08 10:06:57 -05:00
|
|
|
|
2022-09-30 06:35:00 -04:00
|
|
|
def valid_redemption_count
|
|
|
|
if self.redemption_count > self.max_redemptions_allowed
|
|
|
|
errors.add(
|
|
|
|
:redemption_count,
|
|
|
|
I18n.t(
|
|
|
|
"invite.redemption_count_less_than_max",
|
|
|
|
max_redemptions_allowed: self.max_redemptions_allowed,
|
2023-01-09 07:20:10 -05:00
|
|
|
),
|
2022-09-30 06:35:00 -04:00
|
|
|
)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-12-08 10:06:57 -05:00
|
|
|
def valid_domain
|
|
|
|
return if self.domain.blank?
|
|
|
|
|
|
|
|
self.domain.downcase!
|
|
|
|
|
2021-12-13 00:39:14 -05:00
|
|
|
if self.domain !~ Invite::DOMAIN_REGEX
|
2022-01-18 07:38:31 -05:00
|
|
|
self.errors.add(:base, I18n.t("invite.domain_not_allowed"))
|
2021-12-08 10:06:57 -05:00
|
|
|
end
|
|
|
|
end
|
2022-01-18 07:38:31 -05:00
|
|
|
|
2022-05-10 11:45:43 -04:00
|
|
|
def user_exists_error_msg(email)
|
2023-03-08 10:38:58 -05:00
|
|
|
error_key = SiteSetting.hide_email_address_taken? ? "generic_error_response" : "user_exists"
|
|
|
|
|
|
|
|
I18n.t("invite.#{error_key}", email: CGI.escapeHTML(email))
|
2022-01-18 07:38:31 -05:00
|
|
|
end
|
2013-02-05 14:16:51 -05:00
|
|
|
end
|
2013-05-23 22:48:32 -04:00
|
|
|
|
|
|
|
# == Schema Information
|
|
|
|
#
|
|
|
|
# Table name: invites
|
|
|
|
#
|
2020-06-09 11:19:32 -04: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
|
2021-07-05 18:14:15 -04:00
|
|
|
# email_token :string
|
2021-12-08 10:06:57 -05:00
|
|
|
# domain :string
|
2013-05-23 22:48:32 -04:00
|
|
|
#
|
|
|
|
# Indexes
|
|
|
|
#
|
2014-07-29 13:57:08 -04:00
|
|
|
# index_invites_on_email_and_invited_by_id (email,invited_by_id)
|
2019-07-19 01:59:12 -04:00
|
|
|
# index_invites_on_emailed_status (emailed_status)
|
2013-05-23 22:48:32 -04:00
|
|
|
# index_invites_on_invite_key (invite_key) UNIQUE
|
2019-12-30 01:13:27 -05:00
|
|
|
# index_invites_on_invited_by_id (invited_by_id)
|
2013-05-23 22:48:32 -04:00
|
|
|
#
|