mirror of
https://0xacab.org/sutty/sutty
synced 2025-01-19 14:23:38 +00:00
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
|
class BaseController < ActionController::Base
|
||||||
protect_from_forgery with: :null_session
|
protect_from_forgery with: :null_session
|
||||||
respond_to :json
|
respond_to :json
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def origin
|
||||||
|
request.headers['Origin']
|
||||||
|
end
|
||||||
end
|
end
|
||||||
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,8 +68,10 @@ class Site < ApplicationRecord
|
||||||
end
|
end
|
||||||
|
|
||||||
def hostname
|
def hostname
|
||||||
|
return @hostname if @hostname
|
||||||
|
|
||||||
sub = name || I18n.t('deploys.deploy_local.ejemplo')
|
sub = name || I18n.t('deploys.deploy_local.ejemplo')
|
||||||
if sub.ends_with? '.'
|
@hostname = if sub.ends_with? '.'
|
||||||
sub.gsub(/\.\Z/, '')
|
sub.gsub(/\.\Z/, '')
|
||||||
else
|
else
|
||||||
"#{sub}.#{Site.domain}"
|
"#{sub}.#{Site.domain}"
|
||||||
|
@ -340,6 +342,7 @@ class Site < ApplicationRecord
|
||||||
config.description = description
|
config.description = description
|
||||||
config.title = title
|
config.title = title
|
||||||
config.url = url
|
config.url = url
|
||||||
|
config.hostname = hostname
|
||||||
end
|
end
|
||||||
|
|
||||||
# Migra los archivos a Sutty
|
# 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.
|
# routes, locales, etc. This feature depends on the listen gem.
|
||||||
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
config.file_watcher = ActiveSupport::EventedFileUpdateChecker
|
||||||
|
|
||||||
|
config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] }
|
||||||
config.action_mailer.perform_caching = false
|
config.action_mailer.perform_caching = false
|
||||||
config.action_mailer.delivery_method = :letter_opener
|
config.action_mailer.delivery_method = :letter_opener
|
||||||
config.action_mailer.perform_deliveries = true
|
config.action_mailer.perform_deliveries = true
|
||||||
|
|
|
@ -44,6 +44,7 @@ Rails.application.configure do
|
||||||
# The :test delivery method accumulates sent emails in the
|
# The :test delivery method accumulates sent emails in the
|
||||||
# ActionMailer::Base.deliveries array.
|
# ActionMailer::Base.deliveries array.
|
||||||
config.action_mailer.delivery_method = :test
|
config.action_mailer.delivery_method = :test
|
||||||
|
config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] }
|
||||||
config.action_mailer.default_url_options = { host: 'localhost',
|
config.action_mailer.default_url_options = { host: 'localhost',
|
||||||
port: 3000 }
|
port: 3000 }
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,11 @@ en:
|
||||||
es: Castillian Spanish
|
es: Castillian Spanish
|
||||||
en: English
|
en: English
|
||||||
seconds: '%{seconds} seconds'
|
seconds: '%{seconds} seconds'
|
||||||
|
contact_mailer:
|
||||||
|
subject: '[%site] Contact form'
|
||||||
|
name: 'Name: %{name} (%{pronouns})'
|
||||||
|
contact: 'Contact: %{contact}'
|
||||||
|
gdpr: 'Consent: %{gdpr}'
|
||||||
deploy_mailer:
|
deploy_mailer:
|
||||||
deployed:
|
deployed:
|
||||||
subject: "[Sutty] The site %{site} has been built"
|
subject: "[Sutty] The site %{site} has been built"
|
||||||
|
|
|
@ -38,6 +38,11 @@ es:
|
||||||
es: Castellano
|
es: Castellano
|
||||||
en: Inglés
|
en: Inglés
|
||||||
seconds: '%{seconds} segundos'
|
seconds: '%{seconds} segundos'
|
||||||
|
contact_mailer:
|
||||||
|
subject: '[%{site}] Formulario de contacto'
|
||||||
|
name: 'Nombre: %{name} (%{pronouns})'
|
||||||
|
contact: 'Contacto: %{contact}'
|
||||||
|
gdpr: 'Consentimiento: %{gdpr}'
|
||||||
deploy_mailer:
|
deploy_mailer:
|
||||||
deployed:
|
deployed:
|
||||||
subject: "[Sutty] El sitio %{site} ha sido generado"
|
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
|
resources :sites, only: %i[index], constraints: { site_id: /[a-z0-9\-\.]+/, id: /[a-z0-9\-\.]+/ } do
|
||||||
get 'invitades/cookie', to: 'invitades#cookie'
|
get 'invitades/cookie', to: 'invitades#cookie'
|
||||||
resources :posts, only: %i[create]
|
resources :posts, only: %i[create]
|
||||||
|
post :contact, to: 'contact#receive'
|
||||||
end
|
end
|
||||||
end
|
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