diff --git a/.gitignore b/.gitignore index 380b9840..0487e3ec 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ /_sites/* /_deploy/* +/_private/* /data/* /_storage/* diff --git a/Dockerfile b/Dockerfile index b056debc..2ca5e301 100644 --- a/Dockerfile +++ b/Dockerfile @@ -103,6 +103,7 @@ RUN ln -s data/_storage /srv/http/_storage RUN ln -s data/_sites /srv/http/_sites RUN ln -s data/_deploy /srv/http/_deploy RUN ln -s data/_public /srv/http/_public +RUN ln -s data/_private /srv/http/_private # Volver a root para cerrar la compilación USER root diff --git a/app/controllers/private_controller.rb b/app/controllers/private_controller.rb new file mode 100644 index 00000000..a9c5bb96 --- /dev/null +++ b/app/controllers/private_controller.rb @@ -0,0 +1,76 @@ +# Gestiona las versiones privadas de los sitios. Solo se puede acceder +# con una cuenta +class PrivateController < ApplicationController + # XXX: Permite ejecutar JS + skip_forgery_protection + + include Pundit + rescue_from Pundit::NilPolicyError, with: :page_not_found + + # Enviar el archivo si existe, agregar una / al final siempre para no + # romper las direcciones relativas. + def show + authorize site + + # Detectar si necesitamos una / al final + if needs_trailing_slash? + redirect_to request.url + '/' + return + end + + if deploy_private + send_file path, disposition: 'inline' + else + head :not_found + end + end + + private + + # Detects if the URL should have a trailing slash + def needs_trailing_slash? + !trailing_slash? && params[:format].blank? + end + + def trailing_slash? + request.env['REQUEST_URI'].ends_with?('/') + end + + def site + @site ||= find_site + end + + def deploy_private + @deploy_private ||= site.deploys.find_by(type: 'DeployPrivate') + end + + # Devuelve la ruta completa del archivo + def path + return @path if @path + + @path = Pathname.new(File.join(deploy_private.destination, file)).realpath.to_s + + raise Errno::ENOENT unless @path.starts_with? deploy_private.destination + + @path + rescue Errno::ENOENT + File.join(deploy_private.destination, '404.html') + end + + # Devuelve la ruta del archivo, limpieza copiada desde Jekyll + # + # @see Jekyll::URL#sanitize_url + def file + return @file if @file + + @file = params[:file] || '/' + @file += '/' if trailing_slash? + @file += if @file.ends_with? '/' + 'index.html' + else + '.' + params[:format].to_s + end + + @file = @file.gsub('..', '/').gsub('./', '').squeeze('/') + end +end diff --git a/app/models/deploy_private.rb b/app/models/deploy_private.rb new file mode 100644 index 00000000..8535bc82 --- /dev/null +++ b/app/models/deploy_private.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Permite generar el sitio en una versión privada, mostrando información +# que no se vería públicamente (borradores, campos privados, etc.) +# +# XXX: La plantilla tiene que soportar esto con el plugin +# jekyll-private-data +class DeployPrivate < DeployLocal + # No es necesario volver a instalar dependencias + def deploy + jekyll_build + end + + # Hacer el deploy a un directorio privado + def destination + File.join(Rails.root, '_private', site.name) + end + + # No usar recursos en compresión y habilitar los datos privados + def env + @env ||= super.merge({ 'JEKYLL_ENV' => 'development', 'JEKYLL_PRIVATE' => 'true' }) + end +end diff --git a/app/models/site.rb b/app/models/site.rb index 64312a9f..8f74b3c8 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -15,7 +15,7 @@ class Site < ApplicationRecord # TODO: Hacer que los diferentes tipos de deploy se auto registren # @see app/services/site_service.rb - DEPLOYS = %i[local www zip hidden_service].freeze + DEPLOYS = %i[local private www zip hidden_service].freeze validates :name, uniqueness: true, hostname: { allow_root_label: true diff --git a/app/policies/site_policy.rb b/app/policies/site_policy.rb index 1177596c..33bdb2af 100644 --- a/app/policies/site_policy.rb +++ b/app/policies/site_policy.rb @@ -14,6 +14,11 @@ class SitePolicy true end + # Puede ver la versión privada del sitio? + def private? + edit? && site.deploys.find_by_type('DeployPrivate') + end + # Todes les usuaries pueden ver el sitio si aceptaron la invitación def show? !current_role.temporal diff --git a/app/views/deploys/_deploy_private.haml b/app/views/deploys/_deploy_private.haml new file mode 100644 index 00000000..e1a46052 --- /dev/null +++ b/app/views/deploys/_deploy_private.haml @@ -0,0 +1,20 @@ +-# Formulario para alojar una copia privada + +.row + .col + = deploy.hidden_field :id + = deploy.hidden_field :type + + .custom-control.custom-switch + -# + El checkbox invierte la lógica de destrucción porque queremos + crear el deploy si está activado y destruirlo si está + desactivado. + = deploy.check_box :_destroy, + { checked: deploy.object.persisted?, class: 'custom-control-input' }, + '0', '1' + = deploy.label :_destroy, class: 'custom-control-label' do + %h3= t('.title') + = sanitize_markdown t('.help'), + tags: %w[p strong em a] +%hr/ diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index 4ed451ec..a77705ac 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -17,8 +17,10 @@ new_site_post_path(@site, layout: layout) - if policy(@site).edit? - = link_to t('sites.edit.btn', site: @site.title), - edit_site_path(@site), class: 'btn' + = link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn' + + - if policy(@site).private? + = link_to t('sites.private'), '../private/' + @site.name, class: 'btn', target: '_blank' = render 'sites/build', site: @site diff --git a/config/locales/en.yml b/config/locales/en.yml index 6f2562c6..09fe29a7 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -71,6 +71,10 @@ en: title: Link to www success: Success! error: Error + deploy_private: + title: Private version + success: Success! + error: Error deploy_zip: title: Build ZIP file success: Available for download @@ -255,6 +259,13 @@ en: We're working out the details for allowing your own site domains, you can help us! ejemplo: 'example' + deploy_private: + title: 'Generate private version' + help: | + Some templates support gathering private information. By + enabling this option, when changes are published, you and your + collaborators will be able to access this information in a + private copy of the site. deploy_www: title: 'Add www to the address' help: | @@ -337,6 +348,7 @@ en: title: 'Sites' enqueued: 'Waiting for build' enqueue: 'Publish all changes' + private: 'Private version' failed: 'Failed!' build_log: 'Read log' invitations: diff --git a/config/locales/es.yml b/config/locales/es.yml index ee25cc1b..097ee2fc 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -79,6 +79,10 @@ es: title: Alojar como servicio oculto de Tor success: ¡Éxito! error: Hubo un error + deploy_private: + title: Versión privada del sitio + success: ¡Éxito! + error: Hubo un error help: Por cualquier duda, responde este correo para contactarte con nosotres. maintenance_mailer: notice: @@ -257,6 +261,13 @@ es: Estamos desarrollando la posibilidad de agregar tus propios dominios, ¡ayudanos! ejemplo: 'ejemplo' + deploy_private: + title: 'Generar versión privada' + help: | + Algunas plantillas contienen información privada, activando esta + opción, al publicar los cambios podrás acceder a una versión + privada del sitio, que solo estará accesible para todes les + colaboradores del sitio. deploy_www: title: 'Agregar www a la dirección' help: | @@ -339,6 +350,7 @@ es: title: 'Sitios' enqueued: 'Esperando publicación' enqueue: 'Publicar todos los cambios' + private: 'Versión privada' failed: '¡Falló!' build_log: 'Ver registro' invitations: diff --git a/config/routes.rb b/config/routes.rb index 20ddf5f6..22ff6a7f 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -26,9 +26,12 @@ Rails.application.routes.draw do end end - resources :sites, constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do - get 'public/:type/:basename', to: 'sites#send_public_file' + # Las rutas privadas empiezan con una ruta única para poder hacer un + # alias en nginx sin tener que usar expresiones regulares para + # detectar el nombre del sitio. + get '/sites/private/:site_id(*file)', to: 'private#show', constraints: { site_id: %r{[^/]+} } + resources :sites, constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do # Gestionar actualizaciones del sitio get 'pull', to: 'sites#fetch' post 'pull', to: 'sites#merge'