From 1d3e5bdd24f25e1870f3d3bec1c81040a423f138 Mon Sep 17 00:00:00 2001 From: Fire Demon Date: Wed, 26 Aug 2020 13:56:20 -0500 Subject: [Spam Prevention] Add username confirmation and simple, frictionless anti-spam mechanism --- .../admin/pending_accounts_controller.rb | 6 ++--- app/controllers/auth/registrations_controller.rb | 8 +++++- app/models/user.rb | 30 ++++++++++++++++++++-- app/views/about/_registration.html.haml | 5 +++- app/views/about/show.html.haml | 1 + app/views/admin/accounts/index.html.haml | 2 +- app/views/admin/pending_accounts/index.html.haml | 2 +- app/views/auth/registrations/new.html.haml | 5 +++- app/workers/scheduler/user_cleanup_scheduler.rb | 5 ++++ 9 files changed, 54 insertions(+), 10 deletions(-) (limited to 'app') 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 -- cgit