mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-14 17:01: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
|
||||
HTTP_BASIC_USER=
|
||||
HTTP_BASIC_PASSWORD=
|
||||
# Blazer
|
||||
BLAZER_DATABASE_URL=
|
||||
BLAZER_SLACK_WEBHOOK_URL=
|
||||
BLAZER_USERNAME=
|
||||
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
|
||||
request.headers['Origin']
|
||||
end
|
||||
|
||||
# El primer sitio es el sitio por defecto
|
||||
#
|
||||
# @return [Site]
|
||||
def default_site
|
||||
Site.first
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -3,30 +3,7 @@
|
|||
module Api
|
||||
module V1
|
||||
# API para formulario de contacto
|
||||
class ContactController < 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 :form_exists?, unless: :performed?
|
||||
before_action :from_is_address?, unless: :performed?
|
||||
before_action :gave_consent?, unless: :performed?
|
||||
|
||||
class ContactController < ProtectedController
|
||||
# Recibe un mensaje a través del formulario de contacto y lo envía
|
||||
# a les usuaries del sitio.
|
||||
#
|
||||
|
@ -50,97 +27,34 @@ module Api
|
|||
|
||||
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?
|
||||
return if contact_params[:from].blank?
|
||||
return unless contact_params[:from].blank?
|
||||
return if EmailAddress.valid? contact_params[:from]
|
||||
|
||||
@reason = 'email_invalid'
|
||||
head :precondition_required
|
||||
end
|
||||
|
||||
# No aceptar nada si no dió su consentimiento
|
||||
def gave_consent?
|
||||
return if contact_params[:consent].present?
|
||||
|
||||
@reason = 'no_consent'
|
||||
head :precondition_required
|
||||
end
|
||||
|
||||
# Los campos que se envían tienen que corresponder con un
|
||||
# formulario de contacto.
|
||||
def form_exists?
|
||||
return if site.form? params[:form]
|
||||
return if form? && site.form?(params[:form])
|
||||
|
||||
@reason = 'form_doesnt_exist'
|
||||
head :precondition_required
|
||||
end
|
||||
|
||||
# Encuentra el sitio o devuelve nulo
|
||||
def site
|
||||
@site ||= Site.find_by(name: site_id)
|
||||
end
|
||||
|
||||
# Parámetros limpios
|
||||
def contact_params
|
||||
@contact_params ||= params.permit(site.form(params[:form]).params)
|
||||
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
|
||||
|
|
|
@ -9,14 +9,17 @@ module Api
|
|||
# anterior, la cookie se renueva.
|
||||
class InvitadesController < BaseController
|
||||
# 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
|
||||
@site, contact = Site.where(name: site_id, contact: true)
|
||||
.pluck(:name, :contact)
|
||||
contact = Site.where(name: site_id, contact: true)
|
||||
.pluck(:contact)
|
||||
.first
|
||||
|
||||
set_cookie if contact
|
||||
|
||||
render file: Rails.root.join('public', '1x1.png'),
|
||||
render file: rails.root.join('public', '1x1.png'),
|
||||
content_type: 'image/png',
|
||||
layout: false
|
||||
end
|
||||
|
@ -26,28 +29,45 @@ module Api
|
|||
# XXX: Prestar atención a que estas acciones sean lo más rápidas
|
||||
# y utilicen la menor cantidad posible de recursos, porque son
|
||||
# un vector de DDOS.
|
||||
@site, anon = Site.where(name: site_id, colaboracion_anonima: true)
|
||||
.pluck(:name, :colaboracion_anonima)
|
||||
anon = Site.where(name: site_id, colaboracion_anonima: true)
|
||||
.pluck(:colaboracion_anonima)
|
||||
.first
|
||||
|
||||
set_cookie if anon
|
||||
|
||||
render json: {}, status: :ok
|
||||
render file: rails.root.join('public', '1x1.png'),
|
||||
content_type: 'image/png',
|
||||
layout: false
|
||||
end
|
||||
|
||||
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
|
||||
# está cifrado para que no lo modifiquen les visitantes
|
||||
#
|
||||
# Enviamos un token de protección CSRF
|
||||
def set_cookie
|
||||
expires = 30.minutes
|
||||
cookies.encrypted[@site] = {
|
||||
headers['Access-Control-Allow-Origin'] = return_origin
|
||||
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,
|
||||
secure: !Rails.env.test?,
|
||||
expires: expires,
|
||||
same_site: :none,
|
||||
same_site: :strict,
|
||||
value: {
|
||||
csrf: form_authenticity_token,
|
||||
expires: (Time.now + expires).to_i
|
||||
|
|
|
@ -2,31 +2,14 @@
|
|||
|
||||
module Api
|
||||
module V1
|
||||
class PostsController < BaseController
|
||||
# Ver doc/anonymous.md
|
||||
skip_forgery_protection
|
||||
# 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.
|
||||
# Recibe artículos desde colaboraciones anónimas
|
||||
class PostsController < ProtectedController
|
||||
# Crea un artículo solo si el sitio admite invitades
|
||||
def create
|
||||
# No procesar nada más si ya se aplicaron todos los filtros
|
||||
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,
|
||||
site: site,
|
||||
usuarie: usuarie)
|
||||
|
@ -34,74 +17,27 @@ module Api
|
|||
service.create_anonymous
|
||||
|
||||
# Redirigir a la URL de agradecimiento
|
||||
redirect_to params[:redirect_to] || site.url
|
||||
redirect_to params[:redirect_to] || origin.to_s
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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?
|
||||
def cookie_is_valid?
|
||||
unless cookies.encrypted[site_id] &&
|
||||
cookies.encrypted[site_id]['expires'] > Time.now.to_i
|
||||
render html: 'cookie_invalid', status: :no_content
|
||||
end
|
||||
def gave_consent?
|
||||
return if params[:consent].present?
|
||||
|
||||
@reason = 'no_consent'
|
||||
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?
|
||||
if valid_authenticity_token? session, cookies.encrypted[site_id]['csrf']
|
||||
return
|
||||
def destination_exists?
|
||||
return if post? && site.layout?(params[:layout])
|
||||
|
||||
@reason = 'layout_doesnt_exist'
|
||||
head :precondition_required
|
||||
end
|
||||
|
||||
render html: 'token_invalid', status: :no_content
|
||||
end
|
||||
|
||||
# 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)
|
||||
def from_is_address?
|
||||
true
|
||||
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
|
||||
|
||||
# Implementa valores por sí o por no
|
||||
#
|
||||
# Esto es increíblemente difícil de lograr que salga bien!
|
||||
class MetadataBoolean < MetadataTemplate
|
||||
def default_value
|
||||
false
|
||||
|
|
|
@ -26,6 +26,7 @@ class Site < ApplicationRecord
|
|||
belongs_to :design
|
||||
belongs_to :licencia
|
||||
|
||||
has_many :log_entries
|
||||
has_many :deploys
|
||||
has_many :build_stats, through: :deploys
|
||||
has_many :roles
|
||||
|
@ -245,7 +246,7 @@ class Site < ApplicationRecord
|
|||
# Crea un Struct dinámico cuyas llaves son los nombres de todos los
|
||||
# layouts. Si pasamos un layout que no existe, obtenemos un
|
||||
# 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|
|
||||
{ name.to_sym => Layout.new(site: self,
|
||||
name: name.to_sym,
|
||||
|
@ -253,6 +254,18 @@ class Site < ApplicationRecord
|
|||
end.inject(:merge))
|
||||
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
|
||||
#
|
||||
# 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
|
||||
|
||||
class Site
|
||||
MigrationAuthor = Struct.new :email, :name, keyword_init: true
|
||||
|
||||
# Obtiene todos los archivos relacionados en artículos del sitio y los
|
||||
# sube a Sutty de forma que los podamos seguir utilizando normalmente
|
||||
# sin casos especiales (ej. soportar archivos locales al repositorio y
|
||||
|
@ -79,9 +77,7 @@ class Site
|
|||
end
|
||||
|
||||
# Guardamos los cambios
|
||||
unless doc.save(validate: false)
|
||||
log.write "#{doc.path.relative} no se pudo guardar\n"
|
||||
end
|
||||
log.write "#{doc.path.relative} no se pudo guardar\n" unless doc.save(validate: false)
|
||||
|
||||
modified << doc.path.absolute
|
||||
end
|
||||
|
@ -99,7 +95,7 @@ class Site
|
|||
private
|
||||
|
||||
def author
|
||||
@author = MigrationAuthor.new email: "sutty@#{Site.domain}",
|
||||
@author ||= GitAuthor.new email: "sutty@#{Site.domain}",
|
||||
name: 'Sutty'
|
||||
end
|
||||
|
||||
|
|
|
@ -20,14 +20,14 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do
|
|||
post
|
||||
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
|
||||
# XXX: Confiamos en el parámetro de idioma porque estamos
|
||||
# verificándolos en Site#posts
|
||||
self.post = site.posts(lang: locale)
|
||||
.build(layout: layout)
|
||||
# Los artículos anónimos siempre son borradores
|
||||
params[:post][:draft] = true
|
||||
params[:draft] = true
|
||||
|
||||
commit(action: :created) if post.update(anon_post_params)
|
||||
post
|
||||
|
|
|
@ -17,10 +17,11 @@ Rails.application.routes.draw do
|
|||
namespace :v1 do
|
||||
resources :csp_reports, only: %i[create]
|
||||
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]
|
||||
get :'invitades/cookie', to: 'invitades#cookie'
|
||||
post :'posts/:layout', to: 'posts#create'
|
||||
|
||||
get :'contact/cookie', to: 'invitades#contact_cookie'
|
||||
post 'contact/:form', to: 'contact#receive'
|
||||
post :'contact/:form', to: 'contact#receive'
|
||||
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