formulario de contacto, closes #120

This commit is contained in:
f 2020-03-23 17:46:19 -03:00
parent 534dbaed28
commit 6aa50520e8
No known key found for this signature in database
GPG key ID: 2AE5A13E321F953D
13 changed files with 272 additions and 5 deletions

View file

@ -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

View 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
View 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

View 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

View file

@ -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

View 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]

View 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]

View file

@ -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

View file

@ -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 }

View file

@ -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"

View file

@ -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"

View file

@ -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

View 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