mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-15 02:11:42 +00:00
recibir colaboraciones anónimas
This commit is contained in:
parent
553c5ee6a9
commit
ab8442d144
14 changed files with 254 additions and 202 deletions
|
@ -9,7 +9,14 @@ REDIS_CLIENT=
|
||||||
# API authentication
|
# API authentication
|
||||||
HTTP_BASIC_USER=
|
HTTP_BASIC_USER=
|
||||||
HTTP_BASIC_PASSWORD=
|
HTTP_BASIC_PASSWORD=
|
||||||
|
# Blazer
|
||||||
BLAZER_DATABASE_URL=
|
BLAZER_DATABASE_URL=
|
||||||
BLAZER_SLACK_WEBHOOK_URL=
|
BLAZER_SLACK_WEBHOOK_URL=
|
||||||
BLAZER_USERNAME=
|
BLAZER_USERNAME=
|
||||||
BLAZER_PASSWORD=
|
BLAZER_PASSWORD=
|
||||||
|
# Guardar los formularios enviados como LogEntries
|
||||||
|
# @see Api::V1::ProtectedController
|
||||||
|
DEBUG_FORMS=
|
||||||
|
# Duración de la Cookie de invitade
|
||||||
|
# @see Api::V1::ProtectedController
|
||||||
|
COOKIE_DURATION=30
|
||||||
|
|
|
@ -24,6 +24,13 @@ module Api
|
||||||
def origin
|
def origin
|
||||||
request.headers['Origin']
|
request.headers['Origin']
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# El primer sitio es el sitio por defecto
|
||||||
|
#
|
||||||
|
# @return [Site]
|
||||||
|
def default_site
|
||||||
|
Site.first
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,30 +3,7 @@
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
# API para formulario de contacto
|
# API para formulario de contacto
|
||||||
class ContactController < BaseController
|
class ContactController < ProtectedController
|
||||||
# Permitir conexiones desde varios sitios, estamos chequeando más
|
|
||||||
# adelante.
|
|
||||||
skip_forgery_protection
|
|
||||||
|
|
||||||
# Aplicar algunos chequeos básicos. Deberíamos registrar de
|
|
||||||
# alguna forma los errores pero tampoco queremos que nos usen
|
|
||||||
# recursos.
|
|
||||||
#
|
|
||||||
# Devolvemos un error 428: Precondition Required dando la
|
|
||||||
# oportunidad a les visitantes de reintentar en el caso de falsos
|
|
||||||
# positivos. No devolvemos contenido para que el servidor web
|
|
||||||
# capture y muestra la página de error específica de cada sitio o
|
|
||||||
# la genérica de Sutty.
|
|
||||||
#
|
|
||||||
# XXX: Ordenar en orden ascendiente según uso de recursos.
|
|
||||||
before_action :cookie_is_valid?, unless: :performed?
|
|
||||||
before_action :valid_authenticity_token_in_cookie?, unless: :performed?
|
|
||||||
before_action :site_exists?, unless: :performed?
|
|
||||||
before_action :site_is_origin?, unless: :performed?
|
|
||||||
before_action :form_exists?, 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
|
# Recibe un mensaje a través del formulario de contacto y lo envía
|
||||||
# a les usuaries del sitio.
|
# a les usuaries del sitio.
|
||||||
#
|
#
|
||||||
|
@ -50,97 +27,34 @@ module Api
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def site_cookie
|
|
||||||
@site_cookie ||= cookies.encrypted[site_id]
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comprueba que no se haya reutilizado una cookie vencida
|
|
||||||
#
|
|
||||||
# XXX: Si el navegador envió una cookie vencida es porque la está
|
|
||||||
# reutilizando, probablemente de forma maliciosa? Pero también
|
|
||||||
# puede ser que haya tardado más de media hora en enviar el
|
|
||||||
# formulario.
|
|
||||||
def cookie_is_valid?
|
|
||||||
head :precondition_required unless (site_cookie.try(:[], 'expires') || 0) > Time.now.to_i
|
|
||||||
end
|
|
||||||
|
|
||||||
# Queremos comprobar que la cookie corresponda con la sesión. La
|
|
||||||
# cookie puede haber vencido, así que es uno de los chequeos más
|
|
||||||
# simples que hacemos.
|
|
||||||
#
|
|
||||||
# TODO: Pensar una forma de redirigir al origen sin vaciar el
|
|
||||||
# formulario para que le usuarie recargue la cookie.
|
|
||||||
def valid_authenticity_token_in_cookie?
|
|
||||||
return if valid_authenticity_token? session, site_cookie['csrf']
|
|
||||||
|
|
||||||
head :precondition_required
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comprueba que el sitio existe
|
|
||||||
#
|
|
||||||
# TODO: Responder con una zip bomb!
|
|
||||||
def site_exists?
|
|
||||||
head :precondition_required if site.nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
# Comprueba que el mensaje fue enviado desde el sitio o uno
|
|
||||||
# de los sitios permitidos.
|
|
||||||
#
|
|
||||||
# XXX: Este header se puede falsificar de todas formas pero al
|
|
||||||
# menos es una trampa.
|
|
||||||
def site_is_origin?
|
|
||||||
return if site.urls(slash: false).any? { |u| origin.to_s.start_with? u }
|
|
||||||
|
|
||||||
head :precondition_required
|
|
||||||
end
|
|
||||||
|
|
||||||
# Detecta si la dirección de contacto es válida. Además es
|
|
||||||
# opcional.
|
|
||||||
def from_is_address?
|
def from_is_address?
|
||||||
return if contact_params[:from].blank?
|
return unless contact_params[:from].blank?
|
||||||
return if EmailAddress.valid? contact_params[:from]
|
return if EmailAddress.valid? contact_params[:from]
|
||||||
|
|
||||||
|
@reason = 'email_invalid'
|
||||||
head :precondition_required
|
head :precondition_required
|
||||||
end
|
end
|
||||||
|
|
||||||
# No aceptar nada si no dió su consentimiento
|
|
||||||
def gave_consent?
|
def gave_consent?
|
||||||
return if contact_params[:consent].present?
|
return if contact_params[:consent].present?
|
||||||
|
|
||||||
|
@reason = 'no_consent'
|
||||||
head :precondition_required
|
head :precondition_required
|
||||||
end
|
end
|
||||||
|
|
||||||
# Los campos que se envían tienen que corresponder con un
|
# Los campos que se envían tienen que corresponder con un
|
||||||
# formulario de contacto.
|
# formulario de contacto.
|
||||||
def form_exists?
|
def form_exists?
|
||||||
return if site.form? params[:form]
|
return if form? && site.form?(params[:form])
|
||||||
|
|
||||||
|
@reason = 'form_doesnt_exist'
|
||||||
head :precondition_required
|
head :precondition_required
|
||||||
end
|
end
|
||||||
|
|
||||||
# Encuentra el sitio o devuelve nulo
|
|
||||||
def site
|
|
||||||
@site ||= Site.find_by(name: site_id)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Parámetros limpios
|
# Parámetros limpios
|
||||||
def contact_params
|
def contact_params
|
||||||
@contact_params ||= params.permit(site.form(params[:form]).params)
|
@contact_params ||= params.permit(site.form(params[:form]).params)
|
||||||
end
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -9,14 +9,17 @@ module Api
|
||||||
# anterior, la cookie se renueva.
|
# anterior, la cookie se renueva.
|
||||||
class InvitadesController < BaseController
|
class InvitadesController < BaseController
|
||||||
# Cookie para el formulario de contacto
|
# Cookie para el formulario de contacto
|
||||||
|
#
|
||||||
|
# Usamos where porque no nos importa encontrar el resultado, todes
|
||||||
|
# les visitantes reciben lo mismo, pero algunes no reciben cookie.
|
||||||
def contact_cookie
|
def contact_cookie
|
||||||
@site, contact = Site.where(name: site_id, contact: true)
|
contact = Site.where(name: site_id, contact: true)
|
||||||
.pluck(:name, :contact)
|
.pluck(:contact)
|
||||||
.first
|
.first
|
||||||
|
|
||||||
set_cookie if contact
|
set_cookie if contact
|
||||||
|
|
||||||
render file: Rails.root.join('public', '1x1.png'),
|
render file: rails.root.join('public', '1x1.png'),
|
||||||
content_type: 'image/png',
|
content_type: 'image/png',
|
||||||
layout: false
|
layout: false
|
||||||
end
|
end
|
||||||
|
@ -26,28 +29,45 @@ module Api
|
||||||
# XXX: Prestar atención a que estas acciones sean lo más rápidas
|
# XXX: Prestar atención a que estas acciones sean lo más rápidas
|
||||||
# y utilicen la menor cantidad posible de recursos, porque son
|
# y utilicen la menor cantidad posible de recursos, porque son
|
||||||
# un vector de DDOS.
|
# un vector de DDOS.
|
||||||
@site, anon = Site.where(name: site_id, colaboracion_anonima: true)
|
anon = Site.where(name: site_id, colaboracion_anonima: true)
|
||||||
.pluck(:name, :colaboracion_anonima)
|
.pluck(:colaboracion_anonima)
|
||||||
.first
|
.first
|
||||||
|
|
||||||
set_cookie if anon
|
set_cookie if anon
|
||||||
|
|
||||||
render json: {}, status: :ok
|
render file: rails.root.join('public', '1x1.png'),
|
||||||
|
content_type: 'image/png',
|
||||||
|
layout: false
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# Genera el Origin correcto a partir de la URL del sitio.
|
||||||
|
#
|
||||||
|
# En desarrollo devuelve el Origin enviado.
|
||||||
|
#
|
||||||
|
# @return [String]
|
||||||
|
def return_origin
|
||||||
|
Rails.env.production? ? Site.find_by(name: site_id).url : origin
|
||||||
|
end
|
||||||
|
|
||||||
# La cookie no es accesible a través de JS y todo su contenido
|
# La cookie no es accesible a través de JS y todo su contenido
|
||||||
# está cifrado para que no lo modifiquen les visitantes
|
# está cifrado para que no lo modifiquen les visitantes
|
||||||
#
|
#
|
||||||
# Enviamos un token de protección CSRF
|
# Enviamos un token de protección CSRF
|
||||||
def set_cookie
|
def set_cookie
|
||||||
expires = 30.minutes
|
headers['Access-Control-Allow-Origin'] = return_origin
|
||||||
cookies.encrypted[@site] = {
|
headers['Access-Control-Allow-Credentials'] = true
|
||||||
|
headers['Vary'] = 'Origin'
|
||||||
|
|
||||||
|
# TODO: Volver configurable por sitio
|
||||||
|
expires = ENV.fetch('COOKIE_DURATION', '30').to_i.minutes
|
||||||
|
|
||||||
|
cookies.encrypted[site_id] = {
|
||||||
httponly: true,
|
httponly: true,
|
||||||
secure: !Rails.env.test?,
|
secure: !Rails.env.test?,
|
||||||
expires: expires,
|
expires: expires,
|
||||||
same_site: :none,
|
same_site: :strict,
|
||||||
value: {
|
value: {
|
||||||
csrf: form_authenticity_token,
|
csrf: form_authenticity_token,
|
||||||
expires: (Time.now + expires).to_i
|
expires: (Time.now + expires).to_i
|
||||||
|
|
|
@ -2,31 +2,14 @@
|
||||||
|
|
||||||
module Api
|
module Api
|
||||||
module V1
|
module V1
|
||||||
class PostsController < BaseController
|
# Recibe artículos desde colaboraciones anónimas
|
||||||
# Ver doc/anonymous.md
|
class PostsController < ProtectedController
|
||||||
skip_forgery_protection
|
# Crea un artículo solo si el sitio admite invitades
|
||||||
# Protecciones antes de procesar los datos
|
|
||||||
before_action :cookie_is_valid?, unless: :performed?
|
|
||||||
before_action :valid_authenticity_token_in_cookie?, unless: :performed?
|
|
||||||
before_action :site_exists_and_is_anonymous?, unless: :performed?
|
|
||||||
before_action :site_is_origin?, unless: :performed?
|
|
||||||
|
|
||||||
# Crea un artículo solo si el sitio es invitado, pero antes
|
|
||||||
# tenemos que averiguar varias cosas:
|
|
||||||
#
|
|
||||||
# * la cookie sea válida
|
|
||||||
# * el token anti CSRF es válido
|
|
||||||
# * el sitio existe
|
|
||||||
# * el sitio admite invitades
|
|
||||||
# * el origen de la petición no es el sitio
|
|
||||||
#
|
|
||||||
# TODO: Definir cuáles van a ser las respuestas para cada error
|
|
||||||
# o si simplemente vamos a aceptarlas sin dar feedback.
|
|
||||||
def create
|
def create
|
||||||
# No procesar nada más si ya se aplicaron todos los filtros
|
# No procesar nada más si ya se aplicaron todos los filtros
|
||||||
return if performed?
|
return if performed?
|
||||||
|
|
||||||
usuarie = Site::Author.new name: 'Anon', email: "anon@#{site.hostname}"
|
usuarie = GitAuthor.new name: 'Anon', email: "anon@#{site.hostname}"
|
||||||
service = PostService.new(params: params,
|
service = PostService.new(params: params,
|
||||||
site: site,
|
site: site,
|
||||||
usuarie: usuarie)
|
usuarie: usuarie)
|
||||||
|
@ -34,74 +17,27 @@ module Api
|
||||||
service.create_anonymous
|
service.create_anonymous
|
||||||
|
|
||||||
# Redirigir a la URL de agradecimiento
|
# Redirigir a la URL de agradecimiento
|
||||||
redirect_to params[:redirect_to] || site.url
|
redirect_to params[:redirect_to] || origin.to_s
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
# Comprueba que no se haya reutilizado una cookie vencida
|
def gave_consent?
|
||||||
#
|
return if params[:consent].present?
|
||||||
# XXX: Si el navegador envió una cookie vencida es porque la está
|
|
||||||
# reutilizando, probablemente de forma maliciosa?
|
@reason = 'no_consent'
|
||||||
def cookie_is_valid?
|
head :precondition_required
|
||||||
unless cookies.encrypted[site_id] &&
|
|
||||||
cookies.encrypted[site_id]['expires'] > Time.now.to_i
|
|
||||||
render html: 'cookie_invalid', status: :no_content
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Queremos comprobar que la cookie corresponda con la sesión. La
|
def destination_exists?
|
||||||
# cookie puede haber vencido, así que es uno de los chequeos más
|
return if post? && site.layout?(params[:layout])
|
||||||
# simples que hacemos.
|
|
||||||
#
|
@reason = 'layout_doesnt_exist'
|
||||||
# TODO: Pensar una forma de redirigir al origen sin vaciar el
|
head :precondition_required
|
||||||
# formulario para que le usuarie recargue la cookie.
|
|
||||||
def valid_authenticity_token_in_cookie?
|
|
||||||
if valid_authenticity_token? session, cookies.encrypted[site_id]['csrf']
|
|
||||||
return
|
|
||||||
end
|
end
|
||||||
|
|
||||||
render html: 'token_invalid', status: :no_content
|
def from_is_address?
|
||||||
end
|
true
|
||||||
|
|
||||||
# El sitio existe y soporta colaboracion anónima
|
|
||||||
#
|
|
||||||
# Pedimos el sitio aunque no lo necesitemos para que la consulta
|
|
||||||
# entre en la caché
|
|
||||||
def site_exists_and_is_anonymous?
|
|
||||||
_, anon = site_anon_pair
|
|
||||||
|
|
||||||
render html: 'site_not_anon', status: :no_content unless anon
|
|
||||||
end
|
|
||||||
|
|
||||||
# El navegador envía la URL del sitio en el encabezado Origin,
|
|
||||||
# queremos comprobar que los datos son enviados desde ahí.
|
|
||||||
def site_is_origin?
|
|
||||||
site, = site_anon_pair
|
|
||||||
|
|
||||||
return if request.headers['Origin'] == "https://#{site}"
|
|
||||||
|
|
||||||
render html: 'site_not_origin', status: :no_content
|
|
||||||
end
|
|
||||||
|
|
||||||
# Solo soy un atajo
|
|
||||||
def site_id
|
|
||||||
@site_id ||= params[:site_id]
|
|
||||||
end
|
|
||||||
|
|
||||||
# La consulta más barata que podemos hacer y la reutilizamos para
|
|
||||||
# que esté en la caché
|
|
||||||
def site_anon_pair
|
|
||||||
Site.where(name: site_id, colaboracion_anonima: true)
|
|
||||||
.pluck(:name, :colaboracion_anonima)
|
|
||||||
.first
|
|
||||||
end
|
|
||||||
|
|
||||||
# Instancia el sitio completo
|
|
||||||
#
|
|
||||||
# XXX: Solo usar después de comprobar que el sitio existe!
|
|
||||||
def site
|
|
||||||
@site ||= Site.find_by(name: site_id)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
140
app/controllers/api/v1/protected_controller.rb
Normal file
140
app/controllers/api/v1/protected_controller.rb
Normal file
|
@ -0,0 +1,140 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module Api
|
||||||
|
module V1
|
||||||
|
# API para recibir información sin autenticación
|
||||||
|
class ProtectedController < BaseController
|
||||||
|
# Permitir conexiones desde varios sitios, estamos chequeando más
|
||||||
|
# adelante.
|
||||||
|
skip_forgery_protection
|
||||||
|
|
||||||
|
# Aplicar algunos chequeos básicos. Deberíamos registrar de
|
||||||
|
# alguna forma los errores pero tampoco queremos que nos usen
|
||||||
|
# recursos.
|
||||||
|
#
|
||||||
|
# Devolvemos un error 428: Precondition Required dando la
|
||||||
|
# oportunidad a les visitantes de reintentar en el caso de falsos
|
||||||
|
# positivos. No devolvemos contenido para que el servidor web
|
||||||
|
# capture y muestra la página de error específica de cada sitio o
|
||||||
|
# la genérica de Sutty.
|
||||||
|
#
|
||||||
|
# XXX: Ordenar en orden ascendiente según uso de recursos.
|
||||||
|
before_action :cookie_is_valid?, unless: :performed?
|
||||||
|
before_action :valid_authenticity_token_in_cookie?, unless: :performed?
|
||||||
|
before_action :site_exists?, unless: :performed?
|
||||||
|
before_action :site_is_origin?, unless: :performed?
|
||||||
|
before_action :destination_exists?, unless: :performed?
|
||||||
|
before_action :from_is_address?, unless: :performed?
|
||||||
|
before_action :gave_consent?, unless: :performed?
|
||||||
|
|
||||||
|
# Reescribir performed? para registrar lo que pasó en el camino a
|
||||||
|
# menos que esté todo bien.
|
||||||
|
def performed?
|
||||||
|
if (performed = super) && response.status >= 400
|
||||||
|
log_entry if ENV['DEBUG_FORMS'].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
performed
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def site_cookie
|
||||||
|
@site_cookie ||= cookies.encrypted[site_id]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comprueba que no se haya reutilizado una cookie vencida
|
||||||
|
#
|
||||||
|
# XXX: Si el navegador envió una cookie vencida es porque la está
|
||||||
|
# reutilizando, probablemente de forma maliciosa? Pero también
|
||||||
|
# puede ser que haya tardado más de media hora en enviar el
|
||||||
|
# formulario.
|
||||||
|
def cookie_is_valid?
|
||||||
|
return if (site_cookie.try(:[], 'expires') || 0) > Time.now.to_i
|
||||||
|
|
||||||
|
@reason = 'expired_or_invalid_cookie'
|
||||||
|
head :precondition_required
|
||||||
|
end
|
||||||
|
|
||||||
|
# Queremos comprobar que la cookie corresponda con la sesión. La
|
||||||
|
# cookie puede haber vencido, así que es uno de los chequeos más
|
||||||
|
# simples que hacemos.
|
||||||
|
#
|
||||||
|
# TODO: Pensar una forma de redirigir al origen sin vaciar el
|
||||||
|
# formulario para que le usuarie recargue la cookie.
|
||||||
|
def valid_authenticity_token_in_cookie?
|
||||||
|
return if valid_authenticity_token? session, site_cookie['csrf']
|
||||||
|
|
||||||
|
@reason = 'invalid_auth_token'
|
||||||
|
head :precondition_required
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comprueba que el sitio existe
|
||||||
|
#
|
||||||
|
# TODO: Responder con una zip bomb!
|
||||||
|
def site_exists?
|
||||||
|
return unless site.nil?
|
||||||
|
|
||||||
|
@reason = 'site_does_not_exist'
|
||||||
|
head :precondition_required
|
||||||
|
end
|
||||||
|
|
||||||
|
# Comprueba que el mensaje fue enviado desde el sitio o uno
|
||||||
|
# de los sitios permitidos.
|
||||||
|
#
|
||||||
|
# XXX: Este header se puede falsificar de todas formas pero al
|
||||||
|
# menos es una trampa.
|
||||||
|
def site_is_origin?
|
||||||
|
return if site.urls(slash: false).any? { |u| origin.to_s.start_with? u }
|
||||||
|
|
||||||
|
@reason = 'site_is_not_origin'
|
||||||
|
head :precondition_required
|
||||||
|
end
|
||||||
|
|
||||||
|
# Detecta si la dirección de contacto es válida. Además es
|
||||||
|
# opcional.
|
||||||
|
def from_is_address?
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# No aceptar nada si no dió su consentimiento
|
||||||
|
def gave_consent?
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
def post?
|
||||||
|
params[:layout].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
def form?
|
||||||
|
params[:form].present?
|
||||||
|
end
|
||||||
|
|
||||||
|
# Los campos que se envían tienen que corresponder con un
|
||||||
|
# formulario de contacto o un layout
|
||||||
|
def destination_exists?
|
||||||
|
raise NotImplementedError
|
||||||
|
end
|
||||||
|
|
||||||
|
# Encuentra el sitio o devuelve nulo
|
||||||
|
def site
|
||||||
|
@site ||= Site.find_by(name: site_id)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Genera un registro con información básica para debug, quizás no
|
||||||
|
# quede asociado a ningún sitio.
|
||||||
|
#
|
||||||
|
# TODO: Chequear qué pasa con los archivos adjuntos.
|
||||||
|
#
|
||||||
|
# @return [TrueClass]
|
||||||
|
def log_entry
|
||||||
|
LogEntry.create site: site || default_site, text: {
|
||||||
|
reason: @reason,
|
||||||
|
status: response.status,
|
||||||
|
headers: request.headers.to_h.select { |k, _| /\A[A-Z]/ =~ k },
|
||||||
|
params: params
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
10
app/models/log_entry.rb
Normal file
10
app/models/log_entry.rb
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# Un registro de cualquier cosa, nos sirve para debuguear
|
||||||
|
# selectivamente.
|
||||||
|
class LogEntry < ApplicationRecord
|
||||||
|
belongs_to :site
|
||||||
|
serialize :text, JSON
|
||||||
|
|
||||||
|
default_scope -> { order(created_at: :desc) }
|
||||||
|
end
|
|
@ -1,6 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
# Implementa valores por sí o por no
|
# Implementa valores por sí o por no
|
||||||
|
#
|
||||||
|
# Esto es increíblemente difícil de lograr que salga bien!
|
||||||
class MetadataBoolean < MetadataTemplate
|
class MetadataBoolean < MetadataTemplate
|
||||||
def default_value
|
def default_value
|
||||||
false
|
false
|
||||||
|
|
|
@ -26,6 +26,7 @@ class Site < ApplicationRecord
|
||||||
belongs_to :design
|
belongs_to :design
|
||||||
belongs_to :licencia
|
belongs_to :licencia
|
||||||
|
|
||||||
|
has_many :log_entries
|
||||||
has_many :deploys
|
has_many :deploys
|
||||||
has_many :build_stats, through: :deploys
|
has_many :build_stats, through: :deploys
|
||||||
has_many :roles
|
has_many :roles
|
||||||
|
@ -245,7 +246,7 @@ class Site < ApplicationRecord
|
||||||
# Crea un Struct dinámico cuyas llaves son los nombres de todos los
|
# Crea un Struct dinámico cuyas llaves son los nombres de todos los
|
||||||
# layouts. Si pasamos un layout que no existe, obtenemos un
|
# layouts. Si pasamos un layout que no existe, obtenemos un
|
||||||
# NoMethodError
|
# NoMethodError
|
||||||
@layouts_struct ||= Struct.new(*data.fetch('layouts', {}).keys.map(&:to_sym), keyword_init: true)
|
@layouts_struct ||= Struct.new(*layout_keys, keyword_init: true)
|
||||||
@layouts ||= @layouts_struct.new(**data.fetch('layouts', {}).map do |name, metadata|
|
@layouts ||= @layouts_struct.new(**data.fetch('layouts', {}).map do |name, metadata|
|
||||||
{ name.to_sym => Layout.new(site: self,
|
{ name.to_sym => Layout.new(site: self,
|
||||||
name: name.to_sym,
|
name: name.to_sym,
|
||||||
|
@ -253,6 +254,18 @@ class Site < ApplicationRecord
|
||||||
end.inject(:merge))
|
end.inject(:merge))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def layout_keys
|
||||||
|
@layout_keys ||= data.fetch('layouts', {}).keys.map(&:to_sym)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Consulta si el Layout existe
|
||||||
|
#
|
||||||
|
# @param [String,Symbol] El nombre del Layout
|
||||||
|
# @return [Boolean]
|
||||||
|
def layout?(layout)
|
||||||
|
layout_keys.include? layout.to_sym
|
||||||
|
end
|
||||||
|
|
||||||
# Trae todos los valores disponibles para un campo
|
# Trae todos los valores disponibles para un campo
|
||||||
#
|
#
|
||||||
# TODO: Traer recursivamente, si el campo contiene Hash
|
# TODO: Traer recursivamente, si el campo contiene Hash
|
||||||
|
|
|
@ -1,5 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Site
|
|
||||||
Author = Struct.new :email, :name, keyword_init: true
|
|
||||||
end
|
|
|
@ -1,8 +1,6 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class Site
|
class Site
|
||||||
MigrationAuthor = Struct.new :email, :name, keyword_init: true
|
|
||||||
|
|
||||||
# Obtiene todos los archivos relacionados en artículos del sitio y los
|
# Obtiene todos los archivos relacionados en artículos del sitio y los
|
||||||
# sube a Sutty de forma que los podamos seguir utilizando normalmente
|
# sube a Sutty de forma que los podamos seguir utilizando normalmente
|
||||||
# sin casos especiales (ej. soportar archivos locales al repositorio y
|
# sin casos especiales (ej. soportar archivos locales al repositorio y
|
||||||
|
@ -79,9 +77,7 @@ class Site
|
||||||
end
|
end
|
||||||
|
|
||||||
# Guardamos los cambios
|
# Guardamos los cambios
|
||||||
unless doc.save(validate: false)
|
log.write "#{doc.path.relative} no se pudo guardar\n" unless doc.save(validate: false)
|
||||||
log.write "#{doc.path.relative} no se pudo guardar\n"
|
|
||||||
end
|
|
||||||
|
|
||||||
modified << doc.path.absolute
|
modified << doc.path.absolute
|
||||||
end
|
end
|
||||||
|
@ -99,7 +95,7 @@ class Site
|
||||||
private
|
private
|
||||||
|
|
||||||
def author
|
def author
|
||||||
@author = MigrationAuthor.new email: "sutty@#{Site.domain}",
|
@author ||= GitAuthor.new email: "sutty@#{Site.domain}",
|
||||||
name: 'Sutty'
|
name: 'Sutty'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -20,14 +20,14 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
||||||
post
|
post
|
||||||
end
|
end
|
||||||
|
|
||||||
# Crear un post anónimo, con opciones más limitadas
|
# Crear un post anónimo, con opciones más limitadas. No usamos post.
|
||||||
def create_anonymous
|
def create_anonymous
|
||||||
# XXX: Confiamos en el parámetro de idioma porque estamos
|
# XXX: Confiamos en el parámetro de idioma porque estamos
|
||||||
# verificándolos en Site#posts
|
# verificándolos en Site#posts
|
||||||
self.post = site.posts(lang: locale)
|
self.post = site.posts(lang: locale)
|
||||||
.build(layout: layout)
|
.build(layout: layout)
|
||||||
# Los artículos anónimos siempre son borradores
|
# Los artículos anónimos siempre son borradores
|
||||||
params[:post][:draft] = true
|
params[:draft] = true
|
||||||
|
|
||||||
commit(action: :created) if post.update(anon_post_params)
|
commit(action: :created) if post.update(anon_post_params)
|
||||||
post
|
post
|
||||||
|
|
|
@ -17,10 +17,11 @@ Rails.application.routes.draw do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
resources :csp_reports, only: %i[create]
|
resources :csp_reports, only: %i[create]
|
||||||
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]
|
post :'posts/:layout', to: 'posts#create'
|
||||||
|
|
||||||
get :'contact/cookie', to: 'invitades#contact_cookie'
|
get :'contact/cookie', to: 'invitades#contact_cookie'
|
||||||
post 'contact/:form', to: 'contact#receive'
|
post :'contact/:form', to: 'contact#receive'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
11
db/migrate/20200615171026_create_log_entries.rb
Normal file
11
db/migrate/20200615171026_create_log_entries.rb
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class CreateLogEntries < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
create_table :log_entries do |t|
|
||||||
|
t.timestamps
|
||||||
|
t.belongs_to :site, index: true
|
||||||
|
t.text :text
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue