about summary refs log tree commit diff
path: root/app
diff options
context:
space:
mode:
authorFire Demon <firedemon@creature.cafe>2020-08-26 13:56:20 -0500
committerFire Demon <firedemon@creature.cafe>2020-08-30 05:45:20 -0500
commit1d3e5bdd24f25e1870f3d3bec1c81040a423f138 (patch)
tree4c586be72d8ebe144420bdd33e16a7e08e72a643 /app
parent0622450209274a9051c03458156a694f2274f61e (diff)
[Spam Prevention] Add username confirmation and simple, frictionless anti-spam mechanism
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/pending_accounts_controller.rb6
-rw-r--r--app/controllers/auth/registrations_controller.rb8
-rw-r--r--app/models/user.rb30
-rw-r--r--app/views/about/_registration.html.haml5
-rw-r--r--app/views/about/show.html.haml1
-rw-r--r--app/views/admin/accounts/index.html.haml2
-rw-r--r--app/views/admin/pending_accounts/index.html.haml2
-rw-r--r--app/views/auth/registrations/new.html.haml5
-rw-r--r--app/workers/scheduler/user_cleanup_scheduler.rb5
9 files changed, 54 insertions, 10 deletions
diff --git a/app/controllers/admin/pending_accounts_controller.rb b/app/controllers/admin/pending_accounts_controller.rb
index b62a9bc84..8a9a51d84 100644
--- a/app/controllers/admin/pending_accounts_controller.rb
+++ b/app/controllers/admin/pending_accounts_controller.rb
@@ -18,19 +18,19 @@ module Admin
     end
 
     def approve_all
-      Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'approve').save
+      Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.confirmed.pluck(:account_id), action: 'approve').save
       redirect_to admin_pending_accounts_path(current_params)
     end
 
     def reject_all
-      Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.pluck(:account_id), action: 'reject').save
+      Form::AccountBatch.new(current_account: current_account, account_ids: User.pending.confirmed.pluck(:account_id), action: 'reject').save
       redirect_to admin_pending_accounts_path(current_params)
     end
 
     private
 
     def set_accounts
-      @accounts = Account.joins(:user).merge(User.pending.recent).includes(user: :invite_request).page(params[:page])
+      @accounts = Account.joins(:user).merge(User.pending.confirmed.recent).includes(user: :invite_request).page(params[:page])
     end
 
     def form_account_batch_params
diff --git a/app/controllers/auth/registrations_controller.rb b/app/controllers/auth/registrations_controller.rb
index 96d973394..55975b274 100644
--- a/app/controllers/auth/registrations_controller.rb
+++ b/app/controllers/auth/registrations_controller.rb
@@ -35,6 +35,12 @@ class Auth::RegistrationsController < Devise::RegistrationsController
     end
   end
 
+  def create
+    super do |resource|
+      return redirect_to root_path if resource.destroyed?
+    end
+  end
+
   protected
 
   def update_resource(resource, params)
@@ -55,7 +61,7 @@ class Auth::RegistrationsController < Devise::RegistrationsController
 
   def configure_sign_up_params
     devise_parameter_sanitizer.permit(:sign_up) do |u|
-      u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :email, :password, :password_confirmation, :invite_code, :agreement)
+      u.permit({ account_attributes: [:username], invite_request_attributes: [:text] }, :username, :email, :password, :password_confirmation, :kobold, :invite_code, :agreement)
     end
   end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 9d1af7db6..b996c9dbe 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -40,6 +40,8 @@
 #  approved                  :boolean          default(TRUE), not null
 #  sign_in_token             :string
 #  sign_in_token_sent_at     :datetime
+#  username                  :string
+#  kobold                    :string
 #
 
 class User < ApplicationRecord
@@ -87,7 +89,7 @@ class User < ApplicationRecord
   validates :agreement, acceptance: { allow_nil: false, accept: [true, 'true', '1'] }, on: :create
 
   scope :recent, -> { order(id: :desc) }
-  scope :pending, -> { where(approved: false) }
+  scope :pending, -> { where(approved: false).where.not(kobold: '') }
   scope :approved, -> { where(approved: true) }
   scope :confirmed, -> { where.not(confirmed_at: nil) }
   scope :enabled, -> { where(disabled: false) }
@@ -153,7 +155,7 @@ class User < ApplicationRecord
 
     if new_user && approved?
       prepare_new_user!
-    elsif new_user
+    elsif new_user && user_might_not_be_a_spam_bot
       notify_staff_about_pending_account!
     end
   end
@@ -295,6 +297,17 @@ class User < ApplicationRecord
     super
   end
 
+  def send_confirmation_instructions
+    unless user_might_not_be_a_spam_bot
+      invite_request&.destroy
+      account&.destroy
+      destroy
+      return false
+    end
+
+    super
+  end
+
   def reset_password!(new_password, new_password_confirmation)
     return false if encrypted_password.blank?
 
@@ -421,4 +434,17 @@ class User < ApplicationRecord
   def validate_email_dns?
     email_changed? && !(Rails.env.test? || Rails.env.development?)
   end
+
+  def user_might_not_be_a_spam_bot
+    username == account.username && invite_request&.text.present? && kobold_hash_matches?
+  end
+
+  def kobold_hash_matches?
+    kobold.present? && kobold == kobold_hash
+  end
+
+  def kobold_hash
+    value = [account.username, username.downcase, email, invite_request.text].compact.map(&:downcase).join("\u{F0666}")
+    Digest::SHA512.hexdigest(value).upcase
+  end
 end
diff --git a/app/views/about/_registration.html.haml b/app/views/about/_registration.html.haml
index 5d159e9e6..c3bd3ed60 100644
--- a/app/views/about/_registration.html.haml
+++ b/app/views/about/_registration.html.haml
@@ -6,14 +6,17 @@
       = f.simple_fields_for :account do |account_fields|
         = account_fields.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: closed_registrations?
 
+      = f.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_confirmation'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username_confirmation'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: closed_registrations?
       = f.input :email, placeholder: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }, hint: false, disabled: closed_registrations?
       = f.input :password, placeholder: t('simple_form.labels.defaults.password'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.password'), :autocomplete => 'off', :minlength => User.password_length.first, :maxlength => User.password_length.last }, hint: false, disabled: closed_registrations?
       = 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' }, hint: false, disabled: closed_registrations?
+      = f.hidden_field :kobold, input_html: { :autocomplete => 'off' }
+
 
     - if approved_registrations?
       .fields-group
         = f.simple_fields_for :invite_request do |invite_request_fields|
-          = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: false
+          = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: true
 
     .fields-group
       = f.input :agreement, as: :boolean, wrapper: :with_label, label: t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), required: true, disabled: closed_registrations?
diff --git a/app/views/about/show.html.haml b/app/views/about/show.html.haml
index 565c4ed59..b80a9207f 100644
--- a/app/views/about/show.html.haml
+++ b/app/views/about/show.html.haml
@@ -3,6 +3,7 @@
 
 - content_for :header_tags do
   %link{ rel: 'canonical', href: about_url }/
+  %script{ src: '/registration.js', type: 'text/javascript', crossorigin: 'anonymous' }/
   = render partial: 'shared/og'
 
 .landing
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 8eac226e0..bff1f2b20 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -10,7 +10,7 @@
   .filter-subset
     %strong= t('admin.accounts.moderation.title')
     %ul
-      %li= link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), admin_pending_accounts_path
+      %li= link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.confirmed.count)})"], ' '), admin_pending_accounts_path
       %li= filter_link_to t('admin.accounts.moderation.active'), silenced: nil, suspended: nil, pending: nil
       %li= filter_link_to t('admin.accounts.moderation.silenced'), silenced: '1', suspended: nil, pending: nil
       %li= filter_link_to t('admin.accounts.moderation.suspended'), suspended: '1', silenced: nil, pending: nil
diff --git a/app/views/admin/pending_accounts/index.html.haml b/app/views/admin/pending_accounts/index.html.haml
index 8101d7f99..2f73d12b4 100644
--- a/app/views/admin/pending_accounts/index.html.haml
+++ b/app/views/admin/pending_accounts/index.html.haml
@@ -1,5 +1,5 @@
 - content_for :page_title do
-  = t('admin.pending_accounts.title', count: User.pending.count)
+  = t('admin.pending_accounts.title', count: User.pending.confirmed.count)
 
 = form_for(@form, url: batch_admin_pending_accounts_path) do |f|
   = hidden_field_tag :page, params[:page] || 1
diff --git a/app/views/auth/registrations/new.html.haml b/app/views/auth/registrations/new.html.haml
index cc72b87ce..b9033f553 100644
--- a/app/views/auth/registrations/new.html.haml
+++ b/app/views/auth/registrations/new.html.haml
@@ -2,6 +2,7 @@
   = t('auth.register')
 
 - content_for :header_tags do
+  %script{ src: '/registration.js', type: 'text/javascript', crossorigin: 'anonymous' }/
   = render partial: 'shared/og', locals: { description: description_for_sign_up }
 
 = simple_form_for(resource, as: resource_name, url: registration_path(resource_name), html: { novalidate: false }) do |f|
@@ -15,6 +16,7 @@
   = f.simple_fields_for :account do |ff|
     .fields-group
       = ff.input :username, wrapper: :with_label, autofocus: true, label: t('simple_form.labels.defaults.username'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username'), :autocomplete => 'off', pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: t('simple_form.hints.defaults.username', domain: site_hostname)
+      = f.input :username, wrapper: :with_label, label: false, required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.username_confirmation'), :autocomplete => 'off', placeholder: t('simple_form.labels.defaults.username_confirmation'), pattern: '[a-zA-Z0-9_]+', maxlength: 30 }, append: "@#{site_hostname}", hint: false, disabled: closed_registrations?
 
   .fields-group
     = f.input :email, wrapper: :with_label, label: t('simple_form.labels.defaults.email'), required: true, input_html: { 'aria-label' => t('simple_form.labels.defaults.email'), :autocomplete => 'off' }
@@ -28,9 +30,10 @@
   - if approved_registrations? && !@invite.present?
     .fields-group
       = f.simple_fields_for :invite_request, resource.invite_request || resource.build_invite_request do |invite_request_fields|
-        = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: false
+        = invite_request_fields.input :text, as: :text, wrapper: :with_block_label, required: true
 
   = f.input :invite_code, as: :hidden
+  = f.hidden_field :kobold, input_html: { :autocomplete => 'off' }
 
   .fields-group
     = f.input :agreement, as: :boolean, wrapper: :with_label, label: whitelist_mode? ? t('auth.checkbox_agreement_without_rules_html', terms_path: terms_path) : t('auth.checkbox_agreement_html', rules_path: about_more_path, terms_path: terms_path), required: true
diff --git a/app/workers/scheduler/user_cleanup_scheduler.rb b/app/workers/scheduler/user_cleanup_scheduler.rb
index 6113edde1..dade63028 100644
--- a/app/workers/scheduler/user_cleanup_scheduler.rb
+++ b/app/workers/scheduler/user_cleanup_scheduler.rb
@@ -10,5 +10,10 @@ class Scheduler::UserCleanupScheduler
       Account.where(id: batch.map(&:account_id)).delete_all
       User.where(id: batch.map(&:id)).delete_all
     end
+
+    User.where(kobold: '', approved: false).find_in_batches do |batch|
+      Account.where(id: batch.map(&:account_id)).delete_all
+      User.where(id: batch.map(&:id)).delete_all
+    end
   end
 end