Anti-spam measures for volunteering

This commit is contained in:
Petko Bordjukov 2024-04-18 21:11:08 +03:00
parent e461ec504f
commit b8ae2b6b0e
18 changed files with 147 additions and 14 deletions

View File

@ -0,0 +1,20 @@
module Public
class VolunteerConfirmationsController < Public::ApplicationController
def create
@volunteer = Volunteer.find_by!(unique_id: params[:id])
if ActiveSupport::SecurityUtils.secure_compare(@volunteer.confirmation_token, params[:confirmation_token])
@volunteer.transaction do
@volunteer.touch(:confirmed_at)
@volunteer.update(confirmation_token: nil)
end
@volunteer.send_notification_to_volunteer
redirect_to edit_volunteer_path(@volunteer.unique_id), notice: I18n.t("views.volunteers.email_confirmed_successfully")
else
redirect_to root_path, alert: I18n.t("views.volunteers.email_confirmation_error")
end
end
end
end

View File

@ -1,5 +1,6 @@
module Public module Public
class VolunteersController < Public::ApplicationController class VolunteersController < Public::ApplicationController
before_action :check_honey_pot, only: [:create, :edit]
def new def new
@volunteer = current_conference.volunteers.build @volunteer = current_conference.volunteers.build
end end
@ -30,6 +31,10 @@ module Public
private private
def check_honey_pot
head :unauthorized unless params.dig(:volunteer_ht, :full_name).blank?
end
def volunteer_params def volunteer_params
params.require(:volunteer).permit( params.require(:volunteer).permit(
:name, :picture, :email, :phone, :tshirt_size, :tshirt_cut, :name, :picture, :email, :phone, :tshirt_size, :tshirt_cut,

View File

@ -13,8 +13,19 @@ class VolunteerMailer < ActionMailer::Base
I18n.locale = @volunteer.language I18n.locale = @volunteer.language
mail(to: @volunteer.email, mail(to: @volunteer.email,
reply_to: @volunteer.conference.email, reply_to: @volunteer.conference.email,
from: "no-reply@openfest.org", from: "cfp@openfest.org",
subject: I18n.t("volunteer_mailer.success_notification.subject", subject: I18n.t("volunteer_mailer.success_notification.subject",
conference_name: @volunteer.conference.title)) conference_name: @volunteer.conference.title))
end end
def volunteer_email_confirmation(new_volunteer)
@volunteer = new_volunteer
I18n.locale = @volunteer.language
mail(to: @volunteer.email,
reply_to: @volunteer.conference.email,
from: "cfp@openfest.org",
subject: I18n.t("volunteer_mailer.email_confirmation.subject",
conference_name: @volunteer.conference.title))
end
end end

View File

@ -9,7 +9,7 @@ class Volunteer < ActiveRecord::Base
validates :tshirt_size, inclusion: {in: TSHIRT_SIZES.map(&:to_s)} validates :tshirt_size, inclusion: {in: TSHIRT_SIZES.map(&:to_s)}
validates :tshirt_cut, inclusion: {in: TSHIRT_CUTS.map(&:to_s)} validates :tshirt_cut, inclusion: {in: TSHIRT_CUTS.map(&:to_s)}
validates :food_preferences, inclusion: {in: FOOD_PREFERENCES.map(&:to_s)} validates :food_preferences, inclusion: {in: FOOD_PREFERENCES.map(&:to_s)}
validates :email, format: {with: /\A[^@]+@[^@]+\z/}, presence: true validates :email, format: {with: /\A[^@]+@[^@]+\z/}, presence: true, uniqueness: {scope: :conference_id}
validates :phone, presence: true, format: {with: /\A[+\- \(\)0-9]+\z/} validates :phone, presence: true, format: {with: /\A[+\- \(\)0-9]+\z/}
validates :volunteer_team, presence: true validates :volunteer_team, presence: true
validate :volunteer_teams_belong_to_conference validate :volunteer_teams_belong_to_conference
@ -22,7 +22,12 @@ class Volunteer < ActiveRecord::Base
before_create :ensure_main_volunteer_team_is_part_of_additional_volunteer_teams before_create :ensure_main_volunteer_team_is_part_of_additional_volunteer_teams
before_create :assign_unique_id before_create :assign_unique_id
after_create :send_notification_to_volunteer before_create :assign_confirmation_token
after_commit :send_email_confirmation_to_volunteer, on: [:create]
def send_notification_to_volunteer
VolunteerMailer.volunteer_notification(self).deliver_later
end
private private
@ -34,8 +39,12 @@ class Volunteer < ActiveRecord::Base
self.unique_id = Digest::SHA256.hexdigest(SecureRandom.uuid) self.unique_id = Digest::SHA256.hexdigest(SecureRandom.uuid)
end end
def send_notification_to_volunteer def assign_confirmation_token
VolunteerMailer.volunteer_notification(self).deliver_later self.confirmation_token = Digest::SHA256.hexdigest(SecureRandom.uuid)
end
def send_email_confirmation_to_volunteer
VolunteerMailer.volunteer_email_confirmation(self).deliver_later
end end
def volunteer_teams_belong_to_conference def volunteer_teams_belong_to_conference

View File

@ -0,0 +1,5 @@
Здравейте,
Моля, потвърдете e-mail адреса си от следния линк:
<%= confirm_volunteer_url(@volunteer.unique_id, confirmation_token: @volunteer.confirmation_token, host: @volunteer.conference.host_name, protocol: 'https') %>

View File

@ -0,0 +1,5 @@
Hello,
Please confirm your e-mail address by clicking the following link:
<%= confirm_volunteer_url(@volunteer.unique_id, confirmation_token: @volunteer.confirmation_token, host: @volunteer.conference.host_name, protocol: 'https') %>

View File

@ -86,9 +86,14 @@ Rails.application.configure do
# Set this to true and configure the email server for immediate delivery to raise delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false # config.action_mailer.raise_delivery_errors = false
config.action_mailer.delivery_method = :sendmail config.action_mailer.delivery_method = :smtp
config.action_mailer.default_options = {from: "no-reply@openfest.org"} config.action_mailer.default_options = {from: "cfp@openfest.org"}
config.action_mailer.default_url_options = {host: "cfp.openfest.org"} config.action_mailer.default_url_options = {host: "cfp.openfest.org"}
config.action_mailer.smtp_settings = {
address: "host.containers.internal",
enable_starttls_auto: false,
enable_starttls: false
}
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to # Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found). # the I18n.default_locale when a translation cannot be found).

View File

@ -12,7 +12,7 @@ Devise.setup do |config|
# Configure the e-mail address which will be shown in Devise::Mailer, # Configure the e-mail address which will be shown in Devise::Mailer,
# note that it will be overwritten if you use your own mailer class # note that it will be overwritten if you use your own mailer class
# with default "from" parameter. # with default "from" parameter.
config.mailer_sender = "no-reply@openfest.org" config.mailer_sender = "cfp@openfest.org"
# Configure the class responsible to send e-mails. # Configure the class responsible to send e-mails.
# config.mailer = 'Devise::Mailer' # config.mailer = 'Devise::Mailer'

View File

@ -242,6 +242,8 @@ bg:
volunteer_mailer: volunteer_mailer:
success_notification: success_notification:
subject: "Кандидатурата Ви за доброволец за %{conference_name} беше получена" subject: "Кандидатурата Ви за доброволец за %{conference_name} беше получена"
email_confirmation:
subject: "Потвърдете e-mail адреса си, за да се включите в %{conference_name}"
event_states: event_states:
approved: approved:
one: "Одобрено" one: "Одобрено"
@ -412,6 +414,9 @@ bg:
welcome: welcome:
submit_event: "Предложи %{event_type}" submit_event: "Предложи %{event_type}"
volunteers: volunteers:
email_not_confirmed: Вашият e-mail адрес не е потвърден. Моля, проверете електронната си поща и кликнете на линка от полученото писмо за потвърждение.
email_confirmed_successfully: Успешно потвърдихте e-mail адреса си!
email_confirmation_error: Възникна грешка при опит за потвърждаване на e-mail адреса Ви.
new_volunteer_title: Кандидатствай за доброволец new_volunteer_title: Кандидатствай за доброволец
edit_volunteer_title: "Кандидатура за доброволец на %{name}" edit_volunteer_title: "Кандидатура за доброволец на %{name}"
apply: Кандидатствай за доброволец apply: Кандидатствай за доброволец

View File

@ -242,6 +242,8 @@ en:
volunteer_mailer: volunteer_mailer:
success_notification: success_notification:
subject: "Your application for \"volunteership\" on %{conference_name} was received" subject: "Your application for \"volunteership\" on %{conference_name} was received"
email_confirmation:
subject: "Confirm your e-mail address to participate in %{conference_name}"
event_states: event_states:
approved: approved:
one: "Approved" one: "Approved"
@ -412,6 +414,9 @@ en:
welcome: welcome:
submit_event: "Submit %{event_type}" submit_event: "Submit %{event_type}"
volunteers: volunteers:
email_not_confirmed: Your e-mail address has not been confirmed yet. Please check your e-mail and click on the link from the confirmation message you received.
email_confirmed_successfully: You have successfully confirmed your e-mail address!
email_confirmation_error: There was an error during the attempt to confirm your e-mail address.
new_volunteer_title: Apply for a volunteer new_volunteer_title: Apply for a volunteer
edit_volunteer_title: "Volunteership application of %{name}" edit_volunteer_title: "Volunteership application of %{name}"
apply: Apply for a volunteer apply: Apply for a volunteer

View File

@ -11,7 +11,11 @@ Rails.application.routes.draw do
get :confirm get :confirm
end end
end end
resources :volunteers resources :volunteers do
member do
get :confirm, to: "volunteer_confirmations#create"
end
end
resources :volunteer_teams, only: [:index] resources :volunteer_teams, only: [:index]
resources :feedback, as: "conference_feedbacks", controller: "conference_feedbacks", only: [:new, :create, :index] resources :feedback, as: "conference_feedbacks", controller: "conference_feedbacks", only: [:new, :create, :index]
end end

View File

@ -0,0 +1,22 @@
class AddConfirmationAndApprovalToVolunteers < ActiveRecord::Migration[7.1]
class Volunteer < ActiveRecord::Base; end
def change
add_column :volunteers, :confirmation_token, :string
add_index :volunteers, :confirmation_token
add_column :volunteers, :confirmed_at, :timestamptz
add_index :volunteers, :confirmed_at
add_column :volunteers, :approved_at, :timestamptz
add_index :volunteers, :approved_at
reversible do |dir|
dir.up do
execute "UPDATE volunteers SET confirmed_at = created_at, approved_at = created_at;"
end
dir.down do
# no-op
end
end
end
end

View File

@ -1,4 +1,4 @@
#flash_messages { .flash_messages {
border: 1px solid #CCC; border: 1px solid #CCC;
background-color: #F1F1F1; background-color: #F1F1F1;
padding: 10px; padding: 10px;

View File

@ -162,3 +162,14 @@
.btn-link-red:active { .btn-link-red:active {
background: #D27A59; background: #D27A59;
} }
.special-form-field {
transform: scale(0.00069420);
opacity: 0;
position: absolute;
top: 0;
left: 0;
height: 0;
width: 0;
z-index: -1;
}

View File

@ -1,3 +1,3 @@
div#flash_messages div#flash_messages.flash_messages
- flash.each do |key, value| - flash.each do |key, value|
= content_tag :div, value, class: "flash #{key}" = content_tag :div, value, class: "flash #{key}"

View File

@ -9,6 +9,9 @@
= f.hidden_field :picture, value: @volunteer.picture.signed_id if @volunteer.picture.attached? = f.hidden_field :picture, value: @volunteer.picture.signed_id if @volunteer.picture.attached?
= f.input :picture, as: :file, required: false, direct: true, wrapper: false, input_html: {direct_upload: true} = f.input :picture, as: :file, required: false, direct: true, wrapper: false, input_html: {direct_upload: true}
= text_field :volunteer_ht, :full_name, class: 'special-form-field', tabindex: "-1", "aria-hidden": true
= label :volunteer_ht, :full_name, 'Full Name', class: 'special-form-field', "aria-hidden": true
= f.input :name, autofocus: true = f.input :name, autofocus: true
= f.input :email = f.input :email
= f.input :phone, input_html: {value: @volunteer.phone.try(:phony_formatted, format: :international)} = f.input :phone, input_html: {value: @volunteer.phone.try(:phony_formatted, format: :international)}

View File

@ -1,5 +1,9 @@
- content_for(:title) { t 'views.volunteers.edit_volunteer_title', name: @volunteer.name } - content_for(:title) { t 'views.volunteers.edit_volunteer_title', name: @volunteer.name }
- if !@volunteer.new_record? && !@volunteer.confirmed_at
div.flash_messages
div.flash.alert = t 'views.volunteers.email_not_confirmed'
h1 = t 'views.volunteers.edit_volunteer_title', name: @volunteer.name h1 = t 'views.volunteers.edit_volunteer_title', name: @volunteer.name
= render 'form' = render 'form'

View File

@ -1,6 +1,8 @@
require "rails_helper" require "rails_helper"
feature "Volunteering" do feature "Volunteering" do
include ActiveJob::TestHelper
before do before do
Rails.application.load_seed Rails.application.load_seed
sign_in_as_admin sign_in_as_admin
@ -12,12 +14,29 @@ feature "Volunteering" do
visit root_path visit root_path
click_on I18n.t("views.volunteers.apply") click_on I18n.t("views.volunteers.apply")
perform_enqueued_jobs do
fill_in_volunteer_profile fill_in_volunteer_profile
expect(page).to have_content I18n.t("views.volunteers.successful_application") expect(page).to have_content I18n.t("views.volunteers.successful_application")
expect(page).to have_content I18n.t("views.volunteers.email_not_confirmed")
end
perform_enqueued_jobs do
visit link_from_last_email
end
expect(page).not_to have_content I18n.t("views.volunteers.email_not_confirmed")
expect(ActionMailer::Base.deliveries.last.subject).to eq(I18n.t("volunteer_mailer.success_notification.subject", conference_name: "FooConf #{1.year.from_now.year}"))
sign_in_as_admin sign_in_as_admin
click_on_first_conference_in_management_root click_on_first_conference_in_management_root
click_on I18n.t("activerecord.models.volunteership", count: 2).capitalize click_on I18n.t("activerecord.models.volunteership", count: 2).capitalize
expect(page).to have_content "Volunteer Foo" expect(page).to have_content "Volunteer Foo"
end end
private
def link_from_last_email
ActionMailer::Base.deliveries.last.body.to_s.strip[%r{(?<=https://www\.example\.com).*?(?=$)}]
end
end end