Anti-spam measures for volunteering
This commit is contained in:
parent
e461ec504f
commit
8d9d7cc2f3
|
@ -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
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
Здравейте,
|
||||||
|
|
||||||
|
Моля, потвърдете e-mail адреса си от следния линк:
|
||||||
|
|
||||||
|
<%= confirm_volunteer_url(@volunteer.unique_id, confirmation_token: @volunteer.confirmation_token, host: @volunteer.conference.host_name, protocol: 'https') %>
|
|
@ -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') %>
|
|
@ -86,9 +86,12 @@ 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 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).
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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: Кандидатствай за доброволец
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
|
@ -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}"
|
||||||
|
|
|
@ -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)}
|
||||||
|
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue