Merge remote-tracking branch 'origin/master' into gs-master
This commit is contained in:
		
						commit
						e77c3996a5
					
				
							
								
								
									
										33
									
								
								app/controllers/admin/invites_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								app/controllers/admin/invites_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,33 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | module Admin | ||||||
|  |   class InvitesController < BaseController | ||||||
|  |     def index | ||||||
|  |       authorize :invite, :index? | ||||||
|  | 
 | ||||||
|  |       @invites = Invite.includes(user: :account).page(params[:page]) | ||||||
|  |       @invite  = Invite.new | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def create | ||||||
|  |       authorize :invite, :create? | ||||||
|  | 
 | ||||||
|  |       @invite      = Invite.new(resource_params) | ||||||
|  |       @invite.user = current_user | ||||||
|  | 
 | ||||||
|  |       if @invite.save | ||||||
|  |         redirect_to admin_invites_path | ||||||
|  |       else | ||||||
|  |         @invites = Invite.page(params[:page]) | ||||||
|  |         render :index | ||||||
|  |       end | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     def destroy | ||||||
|  |       @invite = Invite.find(params[:id]) | ||||||
|  |       authorize @invite, :destroy? | ||||||
|  |       @invite.expire! | ||||||
|  |       redirect_to admin_invites_path | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -16,6 +16,7 @@ module Admin | |||||||
|       show_staff_badge |       show_staff_badge | ||||||
|       bootstrap_timeline_accounts |       bootstrap_timeline_accounts | ||||||
|       thumbnail |       thumbnail | ||||||
|  |       min_invite_role | ||||||
|     ).freeze |     ).freeze | ||||||
| 
 | 
 | ||||||
|     BOOLEAN_SETTINGS = %w( |     BOOLEAN_SETTINGS = %w( | ||||||
|  | |||||||
| @ -16,13 +16,16 @@ class Auth::RegistrationsController < Devise::RegistrationsController | |||||||
| 
 | 
 | ||||||
|   def build_resource(hash = nil) |   def build_resource(hash = nil) | ||||||
|     super(hash) |     super(hash) | ||||||
|     resource.locale = I18n.locale | 
 | ||||||
|  |     resource.locale      = I18n.locale | ||||||
|  |     resource.invite_code = params[:invite_code] if resource.invite_code.blank? | ||||||
|  | 
 | ||||||
|     resource.build_account if resource.account.nil? |     resource.build_account if resource.account.nil? | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def configure_sign_up_params |   def configure_sign_up_params | ||||||
|     devise_parameter_sanitizer.permit(:sign_up) do |u| |     devise_parameter_sanitizer.permit(:sign_up) do |u| | ||||||
|       u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation) |       u.permit({ account_attributes: [:username] }, :email, :password, :password_confirmation, :invite_code) | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
| @ -35,7 +38,19 @@ class Auth::RegistrationsController < Devise::RegistrationsController | |||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   def check_enabled_registrations |   def check_enabled_registrations | ||||||
|     redirect_to root_path if single_user_mode? || !Setting.open_registrations |     redirect_to root_path if single_user_mode? || !allowed_registrations? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def allowed_registrations? | ||||||
|  |     Setting.open_registrations || (invite_code.present? && Invite.find_by(code: invite_code)&.valid_for_use?) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def invite_code | ||||||
|  |     if params[:user] | ||||||
|  |       params[:user][:invite_code] | ||||||
|  |     else | ||||||
|  |       params[:invite_code] | ||||||
|  |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|   private |   private | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								app/controllers/invites_controller.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								app/controllers/invites_controller.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,43 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class InvitesController < ApplicationController | ||||||
|  |   include Authorization | ||||||
|  | 
 | ||||||
|  |   layout 'admin' | ||||||
|  | 
 | ||||||
|  |   before_action :authenticate_user! | ||||||
|  | 
 | ||||||
|  |   def index | ||||||
|  |     authorize :invite, :create? | ||||||
|  | 
 | ||||||
|  |     @invites = Invite.where(user: current_user) | ||||||
|  |     @invite  = Invite.new(expires_in: 1.day.to_i) | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def create | ||||||
|  |     authorize :invite, :create? | ||||||
|  | 
 | ||||||
|  |     @invite      = Invite.new(resource_params) | ||||||
|  |     @invite.user = current_user | ||||||
|  | 
 | ||||||
|  |     if @invite.save | ||||||
|  |       redirect_to invites_path | ||||||
|  |     else | ||||||
|  |       @invites = Invite.where(user: current_user) | ||||||
|  |       render :index | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def destroy | ||||||
|  |     @invite = Invite.where(user: current_user).find(params[:id]) | ||||||
|  |     authorize @invite, :destroy? | ||||||
|  |     @invite.expire! | ||||||
|  |     redirect_to invites_path | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def resource_params | ||||||
|  |     params.require(:invite).permit(:max_uses, :expires_in) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -448,3 +448,19 @@ | |||||||
|     color: $success-green; |     color: $success-green; | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | .name-tag { | ||||||
|  |   display: flex; | ||||||
|  |   align-items: center; | ||||||
|  | 
 | ||||||
|  |   .avatar { | ||||||
|  |     display: block; | ||||||
|  |     margin: 0; | ||||||
|  |     margin-right: 5px; | ||||||
|  |     border-radius: 50%; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .username { | ||||||
|  |     font-weight: 500; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | |||||||
| @ -28,6 +28,8 @@ class Form::AdminSettings | |||||||
|     :show_staff_badge=, |     :show_staff_badge=, | ||||||
|     :bootstrap_timeline_accounts, |     :bootstrap_timeline_accounts, | ||||||
|     :bootstrap_timeline_accounts=, |     :bootstrap_timeline_accounts=, | ||||||
|  |     :min_invite_role, | ||||||
|  |     :min_invite_role=, | ||||||
|     to: Setting |     to: Setting | ||||||
|   ) |   ) | ||||||
| end | end | ||||||
|  | |||||||
							
								
								
									
										45
									
								
								app/models/invite.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								app/models/invite.rb
									
									
									
									
									
										Normal 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 | ||||||
| @ -33,6 +33,7 @@ | |||||||
| #  account_id                :integer          not null | #  account_id                :integer          not null | ||||||
| #  disabled                  :boolean          default(FALSE), not null | #  disabled                  :boolean          default(FALSE), not null | ||||||
| #  moderator                 :boolean          default(FALSE), not null | #  moderator                 :boolean          default(FALSE), not null | ||||||
|  | #  invite_id                 :integer | ||||||
| # | # | ||||||
| 
 | 
 | ||||||
| class User < ApplicationRecord | class User < ApplicationRecord | ||||||
| @ -47,6 +48,7 @@ class User < ApplicationRecord | |||||||
|          otp_number_of_backup_codes: 10 |          otp_number_of_backup_codes: 10 | ||||||
| 
 | 
 | ||||||
|   belongs_to :account, inverse_of: :user, required: true |   belongs_to :account, inverse_of: :user, required: true | ||||||
|  |   belongs_to :invite, counter_cache: :uses | ||||||
|   accepts_nested_attributes_for :account |   accepts_nested_attributes_for :account | ||||||
| 
 | 
 | ||||||
|   has_many :applications, class_name: 'Doorkeeper::Application', as: :owner |   has_many :applications, class_name: 'Doorkeeper::Application', as: :owner | ||||||
| @ -77,6 +79,8 @@ class User < ApplicationRecord | |||||||
|            :reduce_motion, :system_font_ui, :noindex, :theme, |            :reduce_motion, :system_font_ui, :noindex, :theme, | ||||||
|            to: :settings, prefix: :setting, allow_nil: false |            to: :settings, prefix: :setting, allow_nil: false | ||||||
| 
 | 
 | ||||||
|  |   attr_accessor :invite_code | ||||||
|  | 
 | ||||||
|   def confirmed? |   def confirmed? | ||||||
|     confirmed_at.present? |     confirmed_at.present? | ||||||
|   end |   end | ||||||
| @ -95,6 +99,19 @@ class User < ApplicationRecord | |||||||
|     end |     end | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def role?(role) | ||||||
|  |     case role | ||||||
|  |     when 'user' | ||||||
|  |       true | ||||||
|  |     when 'moderator' | ||||||
|  |       staff? | ||||||
|  |     when 'admin' | ||||||
|  |       admin? | ||||||
|  |     else | ||||||
|  |       false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   def disable! |   def disable! | ||||||
|     update!(disabled: true, |     update!(disabled: true, | ||||||
|             last_sign_in_at: current_sign_in_at, |             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 |     session.web_push_subscription.nil? ? nil : session.web_push_subscription.as_payload | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   def invite_code=(code) | ||||||
|  |     self.invite  = Invite.find_by(code: code) unless code.blank? | ||||||
|  |     @invite_code = code | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   protected |   protected | ||||||
| 
 | 
 | ||||||
|   def send_devise_notification(notification, *args) |   def send_devise_notification(notification, *args) | ||||||
|  | |||||||
							
								
								
									
										25
									
								
								app/policies/invite_policy.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								app/policies/invite_policy.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,25 @@ | |||||||
|  | # frozen_string_literal: true | ||||||
|  | 
 | ||||||
|  | class InvitePolicy < ApplicationPolicy | ||||||
|  |   def index? | ||||||
|  |     staff? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def create? | ||||||
|  |     min_required_role? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def destroy? | ||||||
|  |     owner? || staff? | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   private | ||||||
|  | 
 | ||||||
|  |   def owner? | ||||||
|  |     record.user_id == current_user&.id | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   def min_required_role? | ||||||
|  |     current_user&.role?(Setting.min_invite_role) | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -1,7 +1,7 @@ | |||||||
| %li.log-entry | %li.log-entry | ||||||
|   .log-entry__header |   .log-entry__header | ||||||
|     .log-entry__avatar |     .log-entry__avatar | ||||||
|       = image_tag action_log.account.avatar.url(:original), alt: '', width: 48, height: 48, class: 'avatar' |       = image_tag action_log.account.avatar.url(:original), alt: '', width: 40, height: 40, class: 'avatar' | ||||||
|     .log-entry__content |     .log-entry__content | ||||||
|       .log-entry__title |       .log-entry__title | ||||||
|         = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe |         = t("admin.action_logs.actions.#{action_log.action}_#{action_log.target_type.underscore}", name: content_tag(:span, action_log.account.username, class: 'username'), target: content_tag(:span, log_target(action_log), class: 'target')).html_safe | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								app/views/admin/invites/_invite.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								app/views/admin/invites/_invite.html.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | %tr | ||||||
|  |   %td | ||||||
|  |     .name-tag | ||||||
|  |       = image_tag invite.user.account.avatar.url(:original), alt: '', width: 16, height: 16, class: 'avatar' | ||||||
|  |       %span.username= invite.user.account.username | ||||||
|  |   %td | ||||||
|  |     = invite.uses | ||||||
|  |     = " / #{invite.max_uses}" unless invite.max_uses.nil? | ||||||
|  |   %td | ||||||
|  |     - if invite.expires_at.nil? | ||||||
|  |       ∞ | ||||||
|  |     - else | ||||||
|  |       = l invite.expires_at | ||||||
|  |   %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code) | ||||||
|  |   %td= table_link_to 'times', t('invites.delete'), invite_path(invite), method: :delete if policy(invite).destroy? | ||||||
							
								
								
									
										22
									
								
								app/views/admin/invites/index.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								app/views/admin/invites/index.html.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | |||||||
|  | - content_for :page_title do | ||||||
|  |   = t('admin.invites.title') | ||||||
|  | 
 | ||||||
|  | - if policy(:invite).create? | ||||||
|  |   %p= t('invites.prompt') | ||||||
|  | 
 | ||||||
|  |   = render 'invites/form' | ||||||
|  | 
 | ||||||
|  |   %hr/ | ||||||
|  | 
 | ||||||
|  | %table.table | ||||||
|  |   %thead | ||||||
|  |     %tr | ||||||
|  |       %th | ||||||
|  |       %th= t('invites.table.uses') | ||||||
|  |       %th= t('invites.table.expires_at') | ||||||
|  |       %th | ||||||
|  |       %th | ||||||
|  |   %tbody | ||||||
|  |     = render @invites | ||||||
|  | 
 | ||||||
|  | = paginate @invites | ||||||
| @ -32,6 +32,11 @@ | |||||||
| 
 | 
 | ||||||
|   %hr/ |   %hr/ | ||||||
| 
 | 
 | ||||||
|  |   .fields-group | ||||||
|  |     = f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, as: :radio_buttons, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li' | ||||||
|  | 
 | ||||||
|  |   %hr/ | ||||||
|  | 
 | ||||||
|   .fields-group |   .fields-group | ||||||
|     = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } |     = f.input :site_extended_description, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_description_extended.title'), hint: t('admin.settings.site_description_extended.desc_html'), input_html: { rows: 8 } | ||||||
|     = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } |     = f.input :site_terms, wrapper: :with_block_label, as: :text, label: t('admin.settings.site_terms.title'), hint: t('admin.settings.site_terms.desc_html'), input_html: { rows: 8 } | ||||||
|  | |||||||
| @ -16,6 +16,7 @@ | |||||||
|   = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' } |   = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' } | ||||||
|   = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } |   = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off' } | ||||||
|   = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } |   = f.input :password_confirmation, placeholder: t('simple_form.labels.defaults.confirm_password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.confirm_password'), :autocomplete => 'off' } | ||||||
|  |   = f.input :invite_code, as: :hidden | ||||||
| 
 | 
 | ||||||
|   .actions |   .actions | ||||||
|     = f.button :button, t('auth.register'), type: :submit |     = f.button :button, t('auth.register'), type: :submit | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								app/views/invites/_form.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/views/invites/_form.html.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | = simple_form_for(@invite) do |f| | ||||||
|  |   = render 'shared/error_messages', object: @invite | ||||||
|  | 
 | ||||||
|  |   .fields-group | ||||||
|  |     = f.input :max_uses, wrapper: :with_label, collection: [1, 5, 10, 25, 50, 100], label_method: lambda { |num| I18n.t('invites.max_uses', count: num) }, prompt: I18n.t('invites.max_uses_prompt') | ||||||
|  |     = f.input :expires_in, wrapper: :with_label, collection: [30.minutes, 1.hour, 6.hours, 12.hours, 1.day].map(&:to_i), label_method: lambda { |i| I18n.t("invites.expires_in.#{i}") }, prompt: I18n.t('invites.expires_in_prompt') | ||||||
|  | 
 | ||||||
|  |   .actions | ||||||
|  |     = f.button :button, t('invites.generate'), type: :submit | ||||||
							
								
								
									
										11
									
								
								app/views/invites/_invite.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								app/views/invites/_invite.html.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,11 @@ | |||||||
|  | %tr | ||||||
|  |   %td | ||||||
|  |     = invite.uses | ||||||
|  |     = " / #{invite.max_uses}" unless invite.max_uses.nil? | ||||||
|  |   %td | ||||||
|  |     - if invite.expires_at.nil? | ||||||
|  |       ∞ | ||||||
|  |     - else | ||||||
|  |       = l invite.expires_at | ||||||
|  |   %td= table_link_to 'link', public_invite_url(invite_code: invite.code), public_invite_url(invite_code: invite.code) | ||||||
|  |   %td= table_link_to 'times', t('invites.delete'), invite_path(invite), method: :delete if policy(invite).destroy? | ||||||
							
								
								
									
										19
									
								
								app/views/invites/index.html.haml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								app/views/invites/index.html.haml
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | - content_for :page_title do | ||||||
|  |   = t('invites.title') | ||||||
|  | 
 | ||||||
|  | - if policy(:invite).create? | ||||||
|  |   %p= t('invites.prompt') | ||||||
|  | 
 | ||||||
|  |   = render 'form' | ||||||
|  | 
 | ||||||
|  |   %hr/ | ||||||
|  | 
 | ||||||
|  | %table.table | ||||||
|  |   %thead | ||||||
|  |     %tr | ||||||
|  |       %th= t('invites.table.uses') | ||||||
|  |       %th= t('invites.table.expires_at') | ||||||
|  |       %th | ||||||
|  |       %th | ||||||
|  |   %tbody | ||||||
|  |     = render @invites | ||||||
| @ -8,7 +8,7 @@ | |||||||
| 
 | 
 | ||||||
|   = opengraph 'og:site_name', site_title |   = opengraph 'og:site_name', site_title | ||||||
|   = opengraph 'og:type', 'article' |   = opengraph 'og:type', 'article' | ||||||
|   = opengraph 'og:title', "#{@account.display_name} on #{site_hostname}" |   = opengraph 'og:title', "#{@account.display_name.presence || @account.username} on #{site_hostname}" | ||||||
|   = opengraph 'og:url', account_stream_entry_url(@account, @stream_entry) |   = opengraph 'og:url', account_stream_entry_url(@account, @stream_entry) | ||||||
| 
 | 
 | ||||||
|   = render 'stream_entries/og_description', activity: @stream_entry.activity |   = render 'stream_entries/og_description', activity: @stream_entry.activity | ||||||
|  | |||||||
| @ -231,6 +231,8 @@ en: | |||||||
|       reset: Reset |       reset: Reset | ||||||
|       search: Search |       search: Search | ||||||
|       title: Known instances |       title: Known instances | ||||||
|  |     invites: | ||||||
|  |       title: Invites | ||||||
|     reports: |     reports: | ||||||
|       action_taken_by: Action taken by |       action_taken_by: Action taken by | ||||||
|       are_you_sure: Are you sure? |       are_you_sure: Are you sure? | ||||||
| @ -269,6 +271,9 @@ en: | |||||||
|         deletion: |         deletion: | ||||||
|           desc_html: Allow anyone to delete their account |           desc_html: Allow anyone to delete their account | ||||||
|           title: Open account deletion |           title: Open account deletion | ||||||
|  |         min_invite_role: | ||||||
|  |           disabled: No one | ||||||
|  |           title: Allow invitations by | ||||||
|         open: |         open: | ||||||
|           desc_html: Allow anyone to create an account |           desc_html: Allow anyone to create an account | ||||||
|           title: Open registration |           title: Open registration | ||||||
| @ -432,6 +437,25 @@ en: | |||||||
|     match_whole_word: Match whole word |     match_whole_word: Match whole word | ||||||
|     remove: Remove |     remove: Remove | ||||||
|     remove_all: Remove all |     remove_all: Remove all | ||||||
|  |   invites: | ||||||
|  |     delete: Delete | ||||||
|  |     expires_in: | ||||||
|  |       '1800': 30 minutes | ||||||
|  |       '21600': 6 hours | ||||||
|  |       '3600': 1 hour | ||||||
|  |       '43200': 12 hours | ||||||
|  |       '86400': 1 day | ||||||
|  |     expires_in_prompt: Never | ||||||
|  |     generate: Generate | ||||||
|  |     max_uses: | ||||||
|  |       one: 1 use | ||||||
|  |       other: "%{count} uses" | ||||||
|  |     max_uses_prompt: No limit | ||||||
|  |     prompt: Generate and share links with others to grant access to this instance | ||||||
|  |     table: | ||||||
|  |       expires_at: Expires | ||||||
|  |       uses: Uses | ||||||
|  |     title: Invite people | ||||||
|   landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse." |   landing_strip_html: "<strong>%{name}</strong> is a user on %{link_to_root_path}. You can follow them or interact with them if you have an account anywhere in the fediverse." | ||||||
|   landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>. |   landing_strip_signup_html: If you don't, you can <a href="%{sign_up_path}">sign up here</a>. | ||||||
|   media_attachments: |   media_attachments: | ||||||
|  | |||||||
| @ -30,10 +30,12 @@ en: | |||||||
|         data: Data |         data: Data | ||||||
|         display_name: Display name |         display_name: Display name | ||||||
|         email: E-mail address |         email: E-mail address | ||||||
|  |         expires_in: Expire after | ||||||
|         filtered_languages: Filtered languages |         filtered_languages: Filtered languages | ||||||
|         header: Header |         header: Header | ||||||
|         locale: Language |         locale: Language | ||||||
|         locked: Lock account |         locked: Lock account | ||||||
|  |         max_uses: Max number of uses | ||||||
|         new_password: New password |         new_password: New password | ||||||
|         note: Bio |         note: Bio | ||||||
|         otp_attempt: Two-factor code |         otp_attempt: Two-factor code | ||||||
|  | |||||||
| @ -17,6 +17,8 @@ SimpleNavigation::Configuration.run do |navigation| | |||||||
|       settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url |       settings.item :follower_domains, safe_join([fa_icon('users fw'), t('settings.followers')]), settings_follower_domains_url | ||||||
|     end |     end | ||||||
| 
 | 
 | ||||||
|  |     primary.item :invites, safe_join([fa_icon('user-plus fw'), t('invites.title')]), invites_path, if: proc { Setting.min_invite_role == 'user' } | ||||||
|  | 
 | ||||||
|     primary.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url do |development| |     primary.item :development, safe_join([fa_icon('code fw'), t('settings.development')]), settings_applications_url do |development| | ||||||
|       development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications} |       development.item :your_apps, safe_join([fa_icon('list fw'), t('settings.your_apps')]), settings_applications_url, highlights_on: %r{/settings/applications} | ||||||
|     end |     end | ||||||
| @ -25,6 +27,7 @@ SimpleNavigation::Configuration.run do |navigation| | |||||||
|       admin.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url |       admin.item :action_logs, safe_join([fa_icon('bars fw'), t('admin.action_logs.title')]), admin_action_logs_url | ||||||
|       admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} |       admin.item :reports, safe_join([fa_icon('flag fw'), t('admin.reports.title')]), admin_reports_url, highlights_on: %r{/admin/reports} | ||||||
|       admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts} |       admin.item :accounts, safe_join([fa_icon('users fw'), t('admin.accounts.title')]), admin_accounts_url, highlights_on: %r{/admin/accounts} | ||||||
|  |       admin.item :invites, safe_join([fa_icon('user-plus fw'), t('admin.invites.title')]), admin_invites_path | ||||||
|       admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? } |       admin.item :instances, safe_join([fa_icon('cloud fw'), t('admin.instances.title')]), admin_instances_url, highlights_on: %r{/admin/instances}, if: -> { current_user.admin? } | ||||||
|       admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? } |       admin.item :domain_blocks, safe_join([fa_icon('lock fw'), t('admin.domain_blocks.title')]), admin_domain_blocks_url, highlights_on: %r{/admin/domain_blocks}, if: -> { current_user.admin? } | ||||||
|       admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } |       admin.item :email_domain_blocks, safe_join([fa_icon('envelope fw'), t('admin.email_domain_blocks.title')]), admin_email_domain_blocks_url, highlights_on: %r{/admin/email_domain_blocks}, if: -> { current_user.admin? } | ||||||
|  | |||||||
| @ -22,6 +22,10 @@ Rails.application.routes.draw do | |||||||
|   get 'manifest', to: 'manifests#show', defaults: { format: 'json' } |   get 'manifest', to: 'manifests#show', defaults: { format: 'json' } | ||||||
|   get 'intent', to: 'intents#show' |   get 'intent', to: 'intents#show' | ||||||
| 
 | 
 | ||||||
|  |   devise_scope :user do | ||||||
|  |     get '/invite/:invite_code', to: 'auth/registrations#new', as: :public_invite | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   devise_for :users, path: 'auth', controllers: { |   devise_for :users, path: 'auth', controllers: { | ||||||
|     sessions:           'auth/sessions', |     sessions:           'auth/sessions', | ||||||
|     registrations:      'auth/registrations', |     registrations:      'auth/registrations', | ||||||
| @ -106,6 +110,7 @@ Rails.application.routes.draw do | |||||||
|   resources :media,  only: [:show] |   resources :media,  only: [:show] | ||||||
|   resources :tags,   only: [:show] |   resources :tags,   only: [:show] | ||||||
|   resources :emojis, only: [:show] |   resources :emojis, only: [:show] | ||||||
|  |   resources :invites, only: [:index, :create, :destroy] | ||||||
| 
 | 
 | ||||||
|   get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy |   get '/media_proxy/:id/(*any)', to: 'media_proxy#show', as: :media_proxy | ||||||
| 
 | 
 | ||||||
| @ -119,6 +124,7 @@ Rails.application.routes.draw do | |||||||
|     resources :email_domain_blocks, only: [:index, :new, :create, :destroy] |     resources :email_domain_blocks, only: [:index, :new, :create, :destroy] | ||||||
|     resources :action_logs, only: [:index] |     resources :action_logs, only: [:index] | ||||||
|     resource :settings, only: [:edit, :update] |     resource :settings, only: [:edit, :update] | ||||||
|  |     resources :invites, only: [:index, :create, :destroy] | ||||||
| 
 | 
 | ||||||
|     resources :instances, only: [:index] do |     resources :instances, only: [:index] do | ||||||
|       collection do |       collection do | ||||||
|  | |||||||
| @ -17,6 +17,7 @@ defaults: &defaults | |||||||
|   closed_registrations_message: '' |   closed_registrations_message: '' | ||||||
|   open_deletion: true |   open_deletion: true | ||||||
|   timeline_preview: false |   timeline_preview: false | ||||||
|  |   min_invite_role: 'admin' | ||||||
|   show_staff_badge: true |   show_staff_badge: true | ||||||
|   default_sensitive: false |   default_sensitive: false | ||||||
|   unfollow_modal: false |   unfollow_modal: false | ||||||
|  | |||||||
							
								
								
									
										15
									
								
								db/migrate/20171125024930_create_invites.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								db/migrate/20171125024930_create_invites.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | |||||||
|  | class CreateInvites < ActiveRecord::Migration[5.1] | ||||||
|  |   def change | ||||||
|  |     create_table :invites do |t| | ||||||
|  |       t.belongs_to :user, foreign_key: { on_delete: :cascade } | ||||||
|  |       t.string :code, null: false, default: '' | ||||||
|  |       t.datetime :expires_at, null: true, default: nil | ||||||
|  |       t.integer :max_uses, null: true, default: nil | ||||||
|  |       t.integer :uses, null: false, default: 0 | ||||||
|  | 
 | ||||||
|  |       t.timestamps | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     add_index :invites, :code, unique: true | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										5
									
								
								db/migrate/20171125031751_add_invite_id_to_users.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								db/migrate/20171125031751_add_invite_id_to_users.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,5 @@ | |||||||
|  | class AddInviteIdToUsers < ActiveRecord::Migration[5.1] | ||||||
|  |   def change | ||||||
|  |     add_reference :users, :invite, null: true, default: nil, foreign_key: { on_delete: :nullify }, index: false | ||||||
|  |   end | ||||||
|  | end | ||||||
							
								
								
									
										17
									
								
								db/schema.rb
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								db/schema.rb
									
									
									
									
									
								
							| @ -10,7 +10,7 @@ | |||||||
| # | # | ||||||
| # It's strongly recommended that you check this file into your version control system. | # It's strongly recommended that you check this file into your version control system. | ||||||
| 
 | 
 | ||||||
| ActiveRecord::Schema.define(version: 20171122120436) do | ActiveRecord::Schema.define(version: 20171125031751) do | ||||||
| 
 | 
 | ||||||
|   # These are extensions that must be enabled in order to support this database |   # These are extensions that must be enabled in order to support this database | ||||||
|   enable_extension "plpgsql" |   enable_extension "plpgsql" | ||||||
| @ -194,6 +194,18 @@ ActiveRecord::Schema.define(version: 20171122120436) do | |||||||
|     t.bigint "account_id", null: false |     t.bigint "account_id", null: false | ||||||
|   end |   end | ||||||
| 
 | 
 | ||||||
|  |   create_table "invites", force: :cascade do |t| | ||||||
|  |     t.bigint "user_id" | ||||||
|  |     t.string "code", default: "", null: false | ||||||
|  |     t.datetime "expires_at" | ||||||
|  |     t.integer "max_uses" | ||||||
|  |     t.integer "uses", default: 0, null: false | ||||||
|  |     t.datetime "created_at", null: false | ||||||
|  |     t.datetime "updated_at", null: false | ||||||
|  |     t.index ["code"], name: "index_invites_on_code", unique: true | ||||||
|  |     t.index ["user_id"], name: "index_invites_on_user_id" | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|   create_table "list_accounts", force: :cascade do |t| |   create_table "list_accounts", force: :cascade do |t| | ||||||
|     t.bigint "list_id", null: false |     t.bigint "list_id", null: false | ||||||
|     t.bigint "account_id", null: false |     t.bigint "account_id", null: false | ||||||
| @ -484,6 +496,7 @@ ActiveRecord::Schema.define(version: 20171122120436) do | |||||||
|     t.bigint "account_id", null: false |     t.bigint "account_id", null: false | ||||||
|     t.boolean "disabled", default: false, null: false |     t.boolean "disabled", default: false, null: false | ||||||
|     t.boolean "moderator", default: false, null: false |     t.boolean "moderator", default: false, null: false | ||||||
|  |     t.bigint "invite_id" | ||||||
|     t.index ["account_id"], name: "index_users_on_account_id" |     t.index ["account_id"], name: "index_users_on_account_id" | ||||||
|     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true |     t.index ["confirmation_token"], name: "index_users_on_confirmation_token", unique: true | ||||||
|     t.index ["email"], name: "index_users_on_email", unique: true |     t.index ["email"], name: "index_users_on_email", unique: true | ||||||
| @ -525,6 +538,7 @@ ActiveRecord::Schema.define(version: 20171122120436) do | |||||||
|   add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade |   add_foreign_key "follows", "accounts", name: "fk_32ed1b5560", on_delete: :cascade | ||||||
|   add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade |   add_foreign_key "glitch_keyword_mutes", "accounts", on_delete: :cascade | ||||||
|   add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade |   add_foreign_key "imports", "accounts", name: "fk_6db1b6e408", on_delete: :cascade | ||||||
|  |   add_foreign_key "invites", "users", on_delete: :cascade | ||||||
|   add_foreign_key "list_accounts", "accounts", on_delete: :cascade |   add_foreign_key "list_accounts", "accounts", on_delete: :cascade | ||||||
|   add_foreign_key "list_accounts", "follows", on_delete: :cascade |   add_foreign_key "list_accounts", "follows", on_delete: :cascade | ||||||
|   add_foreign_key "list_accounts", "lists", on_delete: :cascade |   add_foreign_key "list_accounts", "lists", on_delete: :cascade | ||||||
| @ -558,5 +572,6 @@ ActiveRecord::Schema.define(version: 20171122120436) do | |||||||
|   add_foreign_key "stream_entries", "accounts", name: "fk_5659b17554", on_delete: :cascade |   add_foreign_key "stream_entries", "accounts", name: "fk_5659b17554", on_delete: :cascade | ||||||
|   add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade |   add_foreign_key "subscriptions", "accounts", name: "fk_9847d1cbb5", on_delete: :cascade | ||||||
|   add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade |   add_foreign_key "users", "accounts", name: "fk_50500f500d", on_delete: :cascade | ||||||
|  |   add_foreign_key "users", "invites", on_delete: :nullify | ||||||
|   add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade |   add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade | ||||||
| end | end | ||||||
|  | |||||||
							
								
								
									
										6
									
								
								spec/fabricators/invite_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								spec/fabricators/invite_fabricator.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,6 @@ | |||||||
|  | Fabricator(:invite) do | ||||||
|  |   user | ||||||
|  |   expires_at nil | ||||||
|  |   max_uses   nil | ||||||
|  |   uses       0 | ||||||
|  | end | ||||||
							
								
								
									
										30
									
								
								spec/models/invite_spec.rb
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								spec/models/invite_spec.rb
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | |||||||
|  | require 'rails_helper' | ||||||
|  | 
 | ||||||
|  | RSpec.describe Invite, type: :model do | ||||||
|  |   describe '#valid_for_use?' do | ||||||
|  |     it 'returns true when there are no limitations' do | ||||||
|  |       invite = Invite.new(max_uses: nil, expires_at: nil) | ||||||
|  |       expect(invite.valid_for_use?).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns true when not expired' do | ||||||
|  |       invite = Invite.new(max_uses: nil, expires_at: 1.hour.from_now) | ||||||
|  |       expect(invite.valid_for_use?).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns false when expired' do | ||||||
|  |       invite = Invite.new(max_uses: nil, expires_at: 1.hour.ago) | ||||||
|  |       expect(invite.valid_for_use?).to be false | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns true when uses still available' do | ||||||
|  |       invite = Invite.new(max_uses: 250, uses: 249, expires_at: nil) | ||||||
|  |       expect(invite.valid_for_use?).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns false when maximum uses reached' do | ||||||
|  |       invite = Invite.new(max_uses: 250, uses: 250, expires_at: nil) | ||||||
|  |       expect(invite.valid_for_use?).to be false | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | end | ||||||
| @ -273,4 +273,47 @@ RSpec.describe User, type: :model do | |||||||
|       expect(user.token_for_app(app)).to be_nil |       expect(user.token_for_app(app)).to be_nil | ||||||
|     end |     end | ||||||
|   end |   end | ||||||
|  | 
 | ||||||
|  |   describe '#role' do | ||||||
|  |     it 'returns admin for admin' do | ||||||
|  |       user = User.new(admin: true) | ||||||
|  |       expect(user.role).to eq 'admin' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns moderator for moderator' do | ||||||
|  |       user = User.new(moderator: true) | ||||||
|  |       expect(user.role).to eq 'moderator' | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns user otherwise' do | ||||||
|  |       user = User.new | ||||||
|  |       expect(user.role).to eq 'user' | ||||||
|  |     end | ||||||
|  |   end | ||||||
|  | 
 | ||||||
|  |   describe '#role?' do | ||||||
|  |     it 'returns false when invalid role requested' do | ||||||
|  |       user = User.new(admin: true) | ||||||
|  |       expect(user.role?('disabled')).to be false | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns true when exact role match' do | ||||||
|  |       user  = User.new | ||||||
|  |       mod   = User.new(moderator: true) | ||||||
|  |       admin = User.new(admin: true) | ||||||
|  | 
 | ||||||
|  |       expect(user.role?('user')).to be true | ||||||
|  |       expect(mod.role?('moderator')).to be true | ||||||
|  |       expect(admin.role?('admin')).to be true | ||||||
|  |     end | ||||||
|  | 
 | ||||||
|  |     it 'returns true when role higher than needed' do | ||||||
|  |       mod   = User.new(moderator: true) | ||||||
|  |       admin = User.new(admin: true) | ||||||
|  | 
 | ||||||
|  |       expect(mod.role?('user')).to be true | ||||||
|  |       expect(admin.role?('user')).to be true | ||||||
|  |       expect(admin.role?('moderator')).to be true | ||||||
|  |     end | ||||||
|  |   end | ||||||
| end | end | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user