0
0
Fork 0

Add consumable invites (#5814)

* Add consumable invites

* Add UI for generating invite codes

* Add tests

* Display max uses and expiration in invites table, delete invite

* Remove unused column and redundant validator

- Default follows not used, probably bad idea
- InviteCodeValidator is redundant because RegistrationsController
  checks invite code validity

* Add admin setting to disable invites

* Add admin UI for invites, configurable role for invite creation

- Admin UI that lists everyone's invites, always available
- Admin setting min_invite_role to control who can invite people
- Non-admin invite UI only visible if users are allowed to

* Do not remove invites from database, expire them instantly
This commit is contained in:
Eugen Rochko 2017-11-27 16:07:59 +01:00 committed by GitHub
parent 0ea4478b68
commit 740f8a95a9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 439 additions and 5 deletions

View file

@ -28,6 +28,8 @@ class Form::AdminSettings
:show_staff_badge=,
:bootstrap_timeline_accounts,
:bootstrap_timeline_accounts=,
:min_invite_role,
:min_invite_role=,
to: Setting
)
end

45
app/models/invite.rb Normal file
View file

@ -0,0 +1,45 @@
# frozen_string_literal: true
# == Schema Information
#
# Table name: invites
#
# id :integer not null, primary key
# user_id :integer
# code :string default(""), not null
# expires_at :datetime
# max_uses :integer
# uses :integer default(0), not null
# created_at :datetime not null
# updated_at :datetime not null
#
class Invite < ApplicationRecord
belongs_to :user, required: true
has_many :users, inverse_of: :invite
before_validation :set_code
attr_reader :expires_in
def expires_in=(interval)
self.expires_at = interval.to_i.seconds.from_now unless interval.blank?
@expires_in = interval
end
def valid_for_use?
(max_uses.nil? || uses < max_uses) && (expires_at.nil? || expires_at >= Time.now.utc)
end
def expire!
touch(:expires_at)
end
private
def set_code
loop do
self.code = ([*('a'..'z'), *('A'..'Z'), *('0'..'9')] - %w(0 1 I l O)).sample(8).join
break if Invite.find_by(code: code).nil?
end
end
end

View file

@ -33,6 +33,7 @@
# account_id :integer not null
# disabled :boolean default(FALSE), not null
# moderator :boolean default(FALSE), not null
# invite_id :integer
#
class User < ApplicationRecord
@ -47,6 +48,7 @@ class User < ApplicationRecord
otp_number_of_backup_codes: 10
belongs_to :account, inverse_of: :user, required: true
belongs_to :invite, counter_cache: :uses
accepts_nested_attributes_for :account
has_many :applications, class_name: 'Doorkeeper::Application', as: :owner
@ -77,6 +79,8 @@ class User < ApplicationRecord
:reduce_motion, :system_font_ui, :noindex, :theme,
to: :settings, prefix: :setting, allow_nil: false
attr_accessor :invite_code
def confirmed?
confirmed_at.present?
end
@ -95,6 +99,19 @@ class User < ApplicationRecord
end
end
def role?(role)
case role
when 'user'
true
when 'moderator'
staff?
when 'admin'
admin?
else
false
end
end
def disable!
update!(disabled: true,
last_sign_in_at: current_sign_in_at,
@ -169,6 +186,11 @@ class User < ApplicationRecord
session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload
end
def invite_code=(code)
self.invite = Invite.find_by(code: code) unless code.blank?
@invite_code = code
end
protected
def send_devise_notification(notification, *args)