formulario de contacto, closes #120
This commit is contained in:
parent
534dbaed28
commit
6aa50520e8
13 changed files with 272 additions and 5 deletions
|
@ -6,6 +6,12 @@ module Api
|
|||
class BaseController < ActionController::Base
|
||||
protect_from_forgery with: :null_session
|
||||
respond_to :json
|
||||
|
||||
private
|
||||
|
||||
def origin
|
||||
request.headers['Origin']
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
93
app/controllers/api/v1/contact_controller.rb
Normal file
93
app/controllers/api/v1/contact_controller.rb
Normal file
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Api
|
||||
module V1
|
||||
# API para formulario de contacto
|
||||
class ContactController < BaseController
|
||||
# Aplicar algunos chequeos básicos. Deberíamos registrar de
|
||||
# alguna forma los errores pero tampoco queremos que nos usen
|
||||
# recursos.
|
||||
#
|
||||
# TODO: Agregar los mismos chequeos que en PostsController
|
||||
before_action :site_exists?, unless: :performed?
|
||||
before_action :site_is_origin?, unless: :performed?
|
||||
before_action :from_is_address?, unless: :performed?
|
||||
before_action :gave_consent?, unless: :performed?
|
||||
|
||||
# Recibe un mensaje a través del formulario de contacto y lo envía
|
||||
# a les usuaries del sitio.
|
||||
#
|
||||
# Tenemos que verificar que el sitio exista y que algunos campos
|
||||
# estén llenos para detener spambots o DDOS. También nos vamos a
|
||||
# estar apoyando en la limitación de peticiones en el servidor web.
|
||||
def receive
|
||||
# No hacer nada si no se pasaron los chequeos
|
||||
return if performed?
|
||||
|
||||
# Si todo salió bien, enviar los correos y redirigir al sitio.
|
||||
# El sitio nos dice a dónde tenemos que ir.
|
||||
ContactJob.perform_async site_id: site.id,
|
||||
**contact_params.to_h.symbolize_keys
|
||||
|
||||
redirect_to contact_params[:redirect] || site.url
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Comprueba que el sitio existe
|
||||
#
|
||||
# TODO: Responder con una zip bomb!
|
||||
def site_exists?
|
||||
render html: body(:site_exists), status: status unless site
|
||||
end
|
||||
|
||||
# Comprueba que el mensaje vino fue enviado desde el sitio
|
||||
def site_is_origin?
|
||||
return if origin.to_s == site.url
|
||||
|
||||
render html: body(:site_is_origin), status: status
|
||||
end
|
||||
|
||||
# Detecta si la dirección de contacto es válida. Además es
|
||||
# opcional.
|
||||
def from_is_address?
|
||||
return unless contact_params[:from]
|
||||
return if EmailAddress.valid? contact_params[:from]
|
||||
|
||||
render html: body(:from_is_address), status: status
|
||||
end
|
||||
|
||||
# No aceptar nada si no dió su consentimiento
|
||||
def gave_consent?
|
||||
return if contact_params[:gdpr].present?
|
||||
|
||||
render html: body(:gave_consent), status: status
|
||||
end
|
||||
|
||||
# Encuentra el sitio
|
||||
def site
|
||||
@site ||= Site.find_by(name: params[:site_id])
|
||||
end
|
||||
|
||||
# Parámetros limpios
|
||||
def contact_params
|
||||
@contact_params ||= params.permit(:gdpr, :name, :pronouns, :from,
|
||||
:contact, :body, :redirect)
|
||||
end
|
||||
|
||||
# Para poder testear, enviamos un mensaje en el cuerpo de la
|
||||
# respuesta
|
||||
#
|
||||
# @param [Any] el mensaje
|
||||
def body(message)
|
||||
return message.to_s if Rails.env.test?
|
||||
end
|
||||
|
||||
# No queremos informar nada a los spammers, pero en testeo
|
||||
# queremos saber por qué. :no_content oculta el cuerpo.
|
||||
def status
|
||||
Rails.env.test? ? :unprocessable_entity : :no_content
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
8
app/jobs/contact_job.rb
Normal file
8
app/jobs/contact_job.rb
Normal file
|
@ -0,0 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Envía los mensajes de contacto
|
||||
class ContactJob < ApplicationJob
|
||||
def perform(**args)
|
||||
ContactMailer.with(**args).notify_usuaries.deliver_now
|
||||
end
|
||||
end
|
33
app/mailers/contact_mailer.rb
Normal file
33
app/mailers/contact_mailer.rb
Normal file
|
@ -0,0 +1,33 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Formulario de contacto
|
||||
class ContactMailer < ApplicationMailer
|
||||
# Enviar el formulario de contacto a todes les usuaries
|
||||
def notify_usuaries
|
||||
# Enviar de a 10 usuaries para minimizar el riesgo que nos
|
||||
# consideren spammers.
|
||||
#
|
||||
# TODO: #i18n. Agrupar usuaries por su idioma
|
||||
usuaries.each_slice(10) do |u|
|
||||
mail to: u,
|
||||
reply_to: params[:from],
|
||||
subject: I18n.t('contact_mailer.subject', site: site.title)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def site
|
||||
@site ||= Site.find params[:site_id]
|
||||
end
|
||||
|
||||
# Trae solo les usuaries definitives para eliminar un vector de ataque
|
||||
# donde alguien crea un sitio, agrega a muches usuaries y les envía
|
||||
# correos.
|
||||
#
|
||||
# TODO: Mover a Site#usuaries
|
||||
def usuaries
|
||||
site.roles.where(rol: 'usuarie', temporal: false).includes(:usuarie)
|
||||
.pluck(:email)
|
||||
end
|
||||
end
|
|
@ -68,12 +68,14 @@ class Site < ApplicationRecord
|
|||
end
|
||||
|
||||
def hostname
|
||||
return @hostname if @hostname
|
||||
|
||||
sub = name || I18n.t('deploys.deploy_local.ejemplo')
|
||||
if sub.ends_with? '.'
|
||||
sub.gsub(/\.\Z/, '')
|
||||
else
|
||||
"#{sub}.#{Site.domain}"
|
||||
end
|
||||
@hostname = if sub.ends_with? '.'
|
||||
sub.gsub(/\.\Z/, '')
|
||||
else
|
||||
"#{sub}.#{Site.domain}"
|
||||
end
|
||||
end
|
||||
|
||||
def url
|
||||
|
@ -340,6 +342,7 @@ class Site < ApplicationRecord
|
|||
config.description = description
|
||||
config.title = title
|
||||
config.url = url
|
||||
config.hostname = hostname
|
||||
end
|
||||
|
||||
# Migra los archivos a Sutty
|
||||
|
|
11
app/views/contact_mailer/notify_usuaries.html.haml
Normal file
11
app/views/contact_mailer/notify_usuaries.html.haml
Normal file
|
@ -0,0 +1,11 @@
|
|||
-#
|
||||
Solo enviamos versión de texto para no aceptar HTML en el formulario
|
||||
de contacto
|
||||
|
||||
%p
|
||||
%strong= I18n.t('contact_mailer.name', name: sanitize(@params[:name]),
|
||||
pronouns: sanitize(@params[:pronouns]))
|
||||
%strong= I18n.t('contact_mailer.contact', contact: sanitize(@params[:contact]))
|
||||
%strong= I18n.t('contact_mailer.gdpr', gdpr: sanitize(@params[:gdpr]))
|
||||
|
||||
%div= sanitize @params[:body]
|
11
app/views/contact_mailer/notify_usuaries.text.haml
Normal file
11
app/views/contact_mailer/notify_usuaries.text.haml
Normal file
|
@ -0,0 +1,11 @@
|
|||
-#
|
||||
Solo enviamos versión de texto para no aceptar HTML en el formulario
|
||||
de contacto
|
||||
|
||||
= I18n.t('contact_mailer.name',
|
||||
name: sanitize(@params[:name]),
|
||||
pronouns: sanitize(@params[:pronouns]))
|
||||
= I18n.t('contact_mailer.contact', contact: sanitize(@params[:contact]))
|
||||
= I18n.t('contact_mailer.gdpr', gdpr: sanitize(@params[:gdpr]))
|
||||
\
|
||||
= sanitize @params[:body]
|
|
@ -64,6 +64,7 @@ Rails.application.configure do
|
|||
# routes, locales, etc. This feature depends on the listen gem.
|
||||
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||
|
||||
config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] }
|
||||
config.action_mailer.perform_caching = false
|
||||
config.action_mailer.delivery_method = :letter_opener
|
||||
config.action_mailer.perform_deliveries = true
|
||||
|
|
|
@ -44,6 +44,7 @@ Rails.application.configure do
|
|||
# The :test delivery method accumulates sent emails in the
|
||||
# ActionMailer::Base.deliveries array.
|
||||
config.action_mailer.delivery_method = :test
|
||||
config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] }
|
||||
config.action_mailer.default_url_options = { host: 'localhost',
|
||||
port: 3000 }
|
||||
|
||||
|
|
|
@ -36,6 +36,11 @@ en:
|
|||
es: Castillian Spanish
|
||||
en: English
|
||||
seconds: '%{seconds} seconds'
|
||||
contact_mailer:
|
||||
subject: '[%site] Contact form'
|
||||
name: 'Name: %{name} (%{pronouns})'
|
||||
contact: 'Contact: %{contact}'
|
||||
gdpr: 'Consent: %{gdpr}'
|
||||
deploy_mailer:
|
||||
deployed:
|
||||
subject: "[Sutty] The site %{site} has been built"
|
||||
|
|
|
@ -38,6 +38,11 @@ es:
|
|||
es: Castellano
|
||||
en: Inglés
|
||||
seconds: '%{seconds} segundos'
|
||||
contact_mailer:
|
||||
subject: '[%{site}] Formulario de contacto'
|
||||
name: 'Nombre: %{name} (%{pronouns})'
|
||||
contact: 'Contacto: %{contact}'
|
||||
gdpr: 'Consentimiento: %{gdpr}'
|
||||
deploy_mailer:
|
||||
deployed:
|
||||
subject: "[Sutty] El sitio %{site} ha sido generado"
|
||||
|
|
|
@ -24,6 +24,7 @@ Rails.application.routes.draw do
|
|||
resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-\.]+/, id: /[a-z0-9\-\.]+/ } do
|
||||
get 'invitades/cookie', to: 'invitades#cookie'
|
||||
resources :posts, only: %i[create]
|
||||
post :contact, to: 'contact#receive'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
89
test/controllers/api/v1/contact_controller_test.rb
Normal file
89
test/controllers/api/v1/contact_controller_test.rb
Normal file
|
@ -0,0 +1,89 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'test_helper'
|
||||
|
||||
module Api
|
||||
module V1
|
||||
class ContactControllerTest < ActionDispatch::IntegrationTest
|
||||
setup do
|
||||
@rol = create :rol
|
||||
@site = @rol.site
|
||||
@usuarie = @rol.usuarie
|
||||
end
|
||||
|
||||
teardown do
|
||||
@site&.destroy
|
||||
end
|
||||
|
||||
test 'el sitio tiene que existir' do
|
||||
@site.destroy
|
||||
|
||||
post v1_site_contact_url(@site),
|
||||
params: {
|
||||
name: SecureRandom.hex,
|
||||
pronouns: SecureRandom.hex,
|
||||
contact: SecureRandom.hex,
|
||||
from: "#{SecureRandom.hex}@sutty.nl",
|
||||
body: SecureRandom.hex,
|
||||
gdpr: true
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity, response.status
|
||||
assert_equal 'site_exists', response.body
|
||||
end
|
||||
|
||||
test 'hay que enviar desde el sitio principal' do
|
||||
post v1_site_contact_url(@site),
|
||||
params: {
|
||||
name: SecureRandom.hex,
|
||||
pronouns: SecureRandom.hex,
|
||||
contact: SecureRandom.hex,
|
||||
from: "#{SecureRandom.hex}@sutty.nl",
|
||||
body: SecureRandom.hex,
|
||||
gdpr: true
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity, response.status
|
||||
assert_equal 'site_is_origin', response.body
|
||||
end
|
||||
|
||||
test 'hay que dar consentimiento' do
|
||||
post v1_site_contact_url(@site),
|
||||
headers: {
|
||||
Origin: @site.url
|
||||
},
|
||||
params: {
|
||||
name: SecureRandom.hex,
|
||||
pronouns: SecureRandom.hex,
|
||||
contact: SecureRandom.hex,
|
||||
from: "#{SecureRandom.hex}@sutty.nl",
|
||||
body: SecureRandom.hex
|
||||
}
|
||||
|
||||
assert_response :unprocessable_entity, response.status
|
||||
assert_equal 'gave_consent', response.body
|
||||
end
|
||||
|
||||
test 'enviar un mensaje genera correos' do
|
||||
10.times do
|
||||
create :rol, site: @site
|
||||
end
|
||||
|
||||
post v1_site_contact_url(@site),
|
||||
headers: {
|
||||
Origin: @site.url
|
||||
},
|
||||
params: {
|
||||
name: SecureRandom.hex,
|
||||
pronouns: SecureRandom.hex,
|
||||
contact: SecureRandom.hex,
|
||||
from: "#{SecureRandom.hex}@sutty.nl",
|
||||
body: SecureRandom.hex,
|
||||
gdpr: true
|
||||
}
|
||||
|
||||
assert_equal 2, ActionMailer::Base.deliveries.size
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue