diff --git a/.env.example b/.env.example index eea8055d..2d888779 100644 --- a/.env.example +++ b/.env.example @@ -1,7 +1,15 @@ -RAILS_ENV=production +RAILS_ENV= IMAP_SERVER= DEFAULT_FROM= SKEL_SUTTY=https://0xacab.org/sutty/skel.sutty.nl -SUTTY=sutty.nl +SUTTY=sutty.local +SUTTY_WITH_PORT=sutty.local:3000 REDIS_SERVER= REDIS_CLIENT= +# API authentication +HTTP_BASIC_USER= +HTTP_BASIC_PASSWORD= +BLAZER_DATABASE_URL= +BLAZER_SLACK_WEBHOOK_URL= +BLAZER_USERNAME= +BLAZER_PASSWORD= diff --git a/.gitignore b/.gitignore index e2387c87..8d7772c3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,6 @@ yarn-debug.log* .yarn-integrity /vendor + +*.key +*.crt diff --git a/.rubocop.yml b/.rubocop.yml index 91d6fc6e..b970b82a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,61 +1,5 @@ AllCops: - TargetRubyVersion: '2.5' + TargetRubyVersion: '2.6' Style/AsciiComments: Enabled: false - -# Sólo existe para molestarnos (?) -Metrics/AbcSize: - Enabled: false - -Metrics/LineLength: - Exclude: - - 'db/schema.rb' - - 'db/migrate/*.rb' - - 'app/models/site.rb' - -Metrics/MethodLength: - Exclude: - - 'db/schema.rb' - - 'db/migrate/*.rb' - - 'app/models/site.rb' - - 'app/controllers/sites_controller.rb' - - 'app/controllers/posts_controller.rb' - - 'app/controllers/invitadxs_controller.rb' - - 'app/controllers/i18n_controller.rb' - - 'app/controllers/collaborations_controller.rb' - - 'app/controllers/usuaries_controller.rb' - - 'app/models/post.rb' - -Metrics/BlockLength: - Exclude: - - 'config/environments/development.rb' - - 'config/environments/production.rb' - - 'config/initializers/devise.rb' - - 'db/schema.rb' - - 'config/routes.rb' - - 'test/controllers/sites_controller_test.rb' - -Metrics/ClassLength: - Exclude: - - 'app/models/site.rb' - - 'app/controllers/posts_controller.rb' - - 'app/controllers/sites_controller.rb' - - 'test/models/post_test.rb' - - 'test/controllers/sites_controller_test.rb' - -Lint/HandleExceptions: - Exclude: - - 'app/controllers/posts_controller.rb' - -Style/GuardClause: - Exclude: - - 'app/controllers/posts_controller.rb' - -Metrics/PerceivedComplexity: - Exclude: - - 'app/controllers/posts_controller.rb' - -Lint/UnreachableCode: - Exclude: - - 'app/policies/post_policy.rb' diff --git a/.ruby-version b/.ruby-version index 35d16fb1..57cf282e 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.5.7 +2.6.5 diff --git a/Dockerfile b/Dockerfile index 143d4043..634c264a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,8 @@ # el mismo repositorio de trabajo. Cuando tengamos CI/CD algunas cosas # como el tarball van a tener que cambiar porque ya vamos a haber hecho # un clone/pull limpio. -FROM sutty/sdk-ruby:latest as build +FROM sutty/oxipng:latest as oxipng +FROM alpine:3.11 as build MAINTAINER "f " ARG RAILS_MASTER_KEY @@ -13,10 +14,21 @@ ENV SECRET_KEY_BASE solo_es_necesaria_para_correr_rake ENV RAILS_ENV production ENV RAILS_MASTER_KEY=$RAILS_MASTER_KEY -# Para compilar los assets en brotli -RUN apk add --no-cache brotli libssh2 +RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake +RUN apk add --no-cache postgresql-libs git yarn brotli libssh2 python -# Empezamos con la usuaria app creada por sdk-ruby +# https://github.com/rubygems/rubygems/issues/2918 +# https://gitlab.alpinelinux.org/alpine/aports/issues/10808 +COPY ./rubygems-platform-musl.patch /tmp/ +RUN cd /usr/lib/ruby/2.6.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch + +# Agregar el usuario +RUN addgroup -g 82 -S www-data +RUN adduser -s /bin/sh -G www-data -h /home/app -D app +RUN install -dm750 -o app -g www-data /home/app/sutty +RUN gem install --no-document bundler:2.0.2 + +# Empezamos con la usuaria app USER app # Vamos a trabajar dentro de este directorio WORKDIR /home/app/sutty @@ -24,25 +36,23 @@ WORKDIR /home/app/sutty # Copiamos solo el Gemfile para poder instalar las gemas necesarias COPY --chown=app:www-data ./Gemfile . COPY --chown=app:www-data ./Gemfile.lock . -# XXX: No usamos la flag --production porque luego no nos deja -# desinstalar las gemas de los assets -# RUN --mount=type=cache,target=/home/app/.ccache \ RUN bundle install --no-cache --path=./vendor --without='test development' # Vaciar la caché -RUN rm vendor/ruby/2.5.0/cache/*.gem -# Limpiar las librerías nativas, esto ahorra más espacio y uso de -# memoria ya que no hay que cargar símbolos que no se van a usar. -RUN find vendor -name "*.so" | xargs -rn 1 strip --strip-unneeded +RUN rm vendor/ruby/2.6.0/cache/*.gem # Copiar el repositorio git COPY --chown=app:www-data ./.git/ ./.git/ -# Hacer un tarball de los archivos desde el repositorio -RUN git archive -o ../sutty.tar.gz HEAD +# Hacer un clon limpio del repositorio en lugar de copiar todos los +# archivos +RUN cd .. && git clone sutty checkout + +WORKDIR /home/app/checkout +# Traer las gemas: +RUN mv ../sutty/vendor ./vendor +RUN mv ../sutty/.bundle ./.bundle -# Extraer archivos necesarios para compilar los assets -RUN tar xf ../sutty.tar.gz Rakefile config app bin yarn.lock package.json # Instalar secretos -COPY --chown=app:www-data ./config/credentials.yml.enc ./config/ +COPY --chown=app:root ./config/credentials.yml.enc ./config/ # Pre-compilar los assets RUN bundle exec rake assets:precompile # Comprimirlos usando brotli @@ -52,71 +62,51 @@ RUN find public -type f -name "*.gz" | sed -re "s/\.gz$//" | xargs -r brotli -k # assets ya están pre-compilados. RUN sed -re "/(uglifier|bootstrap|coffee-rails)/d" -i Gemfile RUN bundle clean +RUN rm -rf ./node_modules ./tmp/cache ./.git # Contenedor final FROM sutty/monit:latest ENV RAILS_ENV production +# Instalar oxipng +COPY --from=oxipng --chown=root:root /root/.cargo/bin/oxipng /usr/bin/oxipng +RUN chmod 755 /usr/bin/oxipng + # Instalar las dependencias, separamos la librería de base de datos para # poder reutilizar este primer paso desde otros contenedores RUN apk add --no-cache libxslt libxml2 tzdata ruby ruby-bundler ruby-json ruby-bigdecimal ruby-rake +RUN apk add --no-cache postgresql-libs libssh2 file rsync git jpegoptim vips # Chequear que la versión de ruby sea la correcta -RUN test "2.5.7" = `ruby -e 'puts RUBY_VERSION'` +RUN test "2.6.5" = `ruby -e 'puts RUBY_VERSION'` + +# https://github.com/rubygems/rubygems/issues/2918 +# https://gitlab.alpinelinux.org/alpine/aports/issues/10808 +COPY ./rubygems-platform-musl.patch /tmp/ +RUN cd /usr/lib/ruby/2.6.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch -RUN apk add --no-cache postgresql-libs libssh2 # Necesitamos yarn para que Jekyll pueda generar los sitios # XXX: Eliminarlo cuando extraigamos la generación de sitios del proceso # principal RUN apk add --no-cache yarn # Instalar foreman para poder correr los servicios -RUN gem install --no-document --no-user-install foreman -RUN apk add --no-cache file +RUN gem install --no-document --no-user-install bundler foreman -# Agregar el grupo del servidor web +# Agregar el grupo del servidor web y la usuaria RUN addgroup -g 82 -S www-data -# Agregar la usuaria RUN adduser -s /bin/sh -G www-data -h /srv/http -D app -# https://github.com/rubygems/rubygems/issues/2918 -# https://gitlab.alpinelinux.org/alpine/aports/issues/10808 -COPY ./rubygems-platform-musl.patch /tmp/ -RUN cd /usr/lib/ruby/2.5.0 && patch -Np 0 -i /tmp/rubygems-platform-musl.patch - # Convertirse en app para instalar USER app -WORKDIR /srv/http - -# Traer los archivos y colocarlos donde van definitivamente -COPY --from=build --chown=app:www-data /home/app/sutty.tar.gz /tmp/ -RUN tar xf /tmp/sutty.tar.gz -# Publicar el código! -RUN mv /tmp/sutty.tar.gz ./public/ - -# Traer los assets compilados y las gemas -COPY --from=build /home/app/sutty/public/assets public/assets -COPY --from=build /home/app/sutty/public/packs public/packs -COPY --from=build /home/app/sutty/vendor vendor -COPY --from=build /home/app/sutty/.bundle .bundle -COPY --from=build /home/app/sutty/Gemfile Gemfile -COPY --from=build /home/app/sutty/Gemfile.lock Gemfile.lock +COPY --from=build --chown=app:www-data /home/app/checkout /srv/http # Volver a root para cerrar la compilación USER root -# Convertir la aplicación en solo lectura -#RUN chown -R root:root /srv/http -#RUN chmod -R o=g /srv/http -#RUN chown -R app:www-data _deploy _sites - # Sincronizar los assets a un directorio compartido -RUN apk add --no-cache rsync -COPY ./sync_assets.sh /usr/local/bin/sync_assets -RUN chmod 755 /usr/local/bin/sync_assets -# Instalar la configuración de monit y comprobarla -RUN install -m 640 -o root -g root ./monit.conf /etc/monit.d/sutty.conf -RUN monit -t -RUN apk add --no-cache git +RUN install -m 755 /srv/http/sync_assets.sh /usr/local/bin/sync_assets +# Instalar la configuración de monit +RUN install -m 640 -o root -g root /srv/http/monit.conf /etc/monit.d/sutty.conf # Mantener estos directorios! VOLUME "/srv/http/_deploy" diff --git a/Gemfile b/Gemfile index e1f61e0a..b9a71077 100644 --- a/Gemfile +++ b/Gemfile @@ -15,7 +15,7 @@ git_source(:github) do |repo_name| end # Cambiar en Dockerfile también -ruby '2.5.7' +ruby '2.6.5' gem 'dotenv-rails', require: 'dotenv/rails-now' @@ -24,7 +24,7 @@ gem 'rails', '~> 6' # Use Puma as the app server gem 'puma' # Use SCSS for stylesheets -gem 'sass-rails', '~> 5.0' +gem 'sassc-rails' # Use Uglifier as compressor for JavaScript assets gem 'uglifier', '>= 1.3.0' # See https://github.com/rails/execjs#readme for more supported runtimes @@ -38,10 +38,7 @@ gem 'turbolinks', '~> 5' gem 'jbuilder', '~> 2.5' # Use ActiveModel has_secure_password gem 'bcrypt', '~> 3.1.7' - -# Use Capistrano for deployment -# gem 'capistrano-rails', group: :development - +gem 'blazer' gem 'bootstrap', '~> 4' gem 'commonmarker' gem 'devise' @@ -83,23 +80,17 @@ end group :development do # Access an IRB console on exception pages or by using <%= console %> # anywhere in the code. - gem 'listen', '>= 3.0.5', '< 3.2' - gem 'web-console', '>= 3.3.0' - # Spring speeds up development by keeping your application running in - # the background. Read more: https://github.com/rails/spring gem 'bcrypt_pbkdf' - gem 'capistrano' - gem 'capistrano-bundler' - gem 'capistrano-passenger' - gem 'capistrano-rails' - gem 'capistrano-rbenv' + gem 'brakeman' gem 'ed25519' gem 'haml-lint', require: false gem 'letter_opener' + gem 'listen', '>= 3.0.5', '< 3.2' gem 'rbnacl', '< 5.0' gem 'rubocop-rails' gem 'spring' gem 'spring-watcher-listen', '~> 2.0.0' + gem 'web-console', '>= 3.3.0' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 9f5db802..c46f3333 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -16,56 +16,56 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.0.1) - actionpack (= 6.0.1) + actioncable (6.0.2.1) + actionpack (= 6.0.2.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.0.1) - actionpack (= 6.0.1) - activejob (= 6.0.1) - activerecord (= 6.0.1) - activestorage (= 6.0.1) - activesupport (= 6.0.1) + actionmailbox (6.0.2.1) + actionpack (= 6.0.2.1) + activejob (= 6.0.2.1) + activerecord (= 6.0.2.1) + activestorage (= 6.0.2.1) + activesupport (= 6.0.2.1) mail (>= 2.7.1) - actionmailer (6.0.1) - actionpack (= 6.0.1) - actionview (= 6.0.1) - activejob (= 6.0.1) + actionmailer (6.0.2.1) + actionpack (= 6.0.2.1) + actionview (= 6.0.2.1) + activejob (= 6.0.2.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.0.1) - actionview (= 6.0.1) - activesupport (= 6.0.1) - rack (~> 2.0) + actionpack (6.0.2.1) + actionview (= 6.0.2.1) + activesupport (= 6.0.2.1) + rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.0.1) - actionpack (= 6.0.1) - activerecord (= 6.0.1) - activestorage (= 6.0.1) - activesupport (= 6.0.1) + actiontext (6.0.2.1) + actionpack (= 6.0.2.1) + activerecord (= 6.0.2.1) + activestorage (= 6.0.2.1) + activesupport (= 6.0.2.1) nokogiri (>= 1.8.5) - actionview (6.0.1) - activesupport (= 6.0.1) + actionview (6.0.2.1) + activesupport (= 6.0.2.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) - activejob (6.0.1) - activesupport (= 6.0.1) + activejob (6.0.2.1) + activesupport (= 6.0.2.1) globalid (>= 0.3.6) - activemodel (6.0.1) - activesupport (= 6.0.1) - activerecord (6.0.1) - activemodel (= 6.0.1) - activesupport (= 6.0.1) - activestorage (6.0.1) - actionpack (= 6.0.1) - activejob (= 6.0.1) - activerecord (= 6.0.1) + activemodel (6.0.2.1) + activesupport (= 6.0.2.1) + activerecord (6.0.2.1) + activemodel (= 6.0.2.1) + activesupport (= 6.0.2.1) + activestorage (6.0.2.1) + actionpack (= 6.0.2.1) + activejob (= 6.0.2.1) + activerecord (= 6.0.2.1) marcel (~> 0.3.1) - activesupport (6.0.1) + activesupport (6.0.2.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -73,34 +73,23 @@ GEM zeitwerk (~> 2.2) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) - airbrussh (1.3.4) - sshkit (>= 1.6.1, != 1.7.0) ast (2.4.0) - autoprefixer-rails (9.6.1.1) + autoprefixer-rails (9.7.3) execjs bcrypt (3.1.13) bcrypt_pbkdf (1.0.1) bindex (0.8.1) - bootstrap (4.3.1) + blazer (2.2.1) + activerecord (>= 5) + chartkick (>= 3.2) + railties (>= 5) + safely_block (>= 0.1.1) + bootstrap (4.4.1) autoprefixer-rails (>= 9.1.0) popper_js (>= 1.14.3, < 2) sassc-rails (>= 2.0.0) - builder (3.2.3) - capistrano (3.11.1) - airbrussh (>= 1.0.0) - i18n - rake (>= 10.0.0) - sshkit (>= 1.9.0) - capistrano-bundler (1.6.0) - capistrano (~> 3.1) - capistrano-passenger (0.2.0) - capistrano (~> 3.0) - capistrano-rails (1.4.0) - capistrano (~> 3.1) - capistrano-bundler (~> 1.1) - capistrano-rbenv (2.1.4) - capistrano (~> 3.1) - sshkit (~> 1.3) + brakeman (4.7.2) + builder (3.2.4) capybara (2.18.0) addressable mini_mime (>= 0.1.3) @@ -108,11 +97,11 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (>= 2.0, < 4.0) - childprocess (2.0.0) - rake (< 13.0) + chartkick (3.3.1) + childprocess (3.0.0) coderay (1.1.2) colorator (1.1.0) - commonmarker (0.20.1) + commonmarker (0.20.2) ruby-enum (~> 0.5) concurrent-ruby (1.1.5) crass (1.0.5) @@ -123,8 +112,8 @@ GEM railties (>= 4.1.0) responders warden (~> 1.2.3) - devise-i18n (1.8.2) - devise (>= 4.6) + devise-i18n (1.9.0) + devise (>= 4.7.1) devise_invitable (2.0.1) actionmailer (>= 5.0) devise (>= 4.6) @@ -136,23 +125,24 @@ GEM em-websocket (0.5.1) eventmachine (>= 0.12.9) http_parser.rb (~> 0.6.0) - email_address (0.1.11) - netaddr (~> 2.0) + email_address (0.1.12) + netaddr (>= 2.0.4, < 3) simpleidn + errbase (0.2.0) erubi (1.9.0) eventmachine (1.2.7) exception_notification (4.4.0) actionmailer (>= 4.0, < 7) activesupport (>= 4.0, < 7) execjs (2.7.0) - factory_bot (5.0.2) + factory_bot (5.1.1) activesupport (>= 4.2.0) - factory_bot_rails (5.0.2) - factory_bot (~> 5.0.2) + factory_bot_rails (5.1.1) + factory_bot (~> 5.1.0) railties (>= 4.2.0) - ffi (1.11.1) + ffi (1.11.3) forwardable-extended (2.6.0) - friendly_id (5.2.5) + friendly_id (5.3.0) activerecord (>= 4.0.0) globalid (0.4.2) activesupport (>= 4.2.0) @@ -161,13 +151,12 @@ GEM tilt haml-lint (0.999.999) haml_lint - haml_lint (0.33.0) + haml_lint (0.34.1) haml (>= 4.0, < 5.2) rainbow - rake (>= 10, < 13) rubocop (>= 0.50.0) sysexits (~> 1.1) - hamlit (2.10.0) + hamlit (2.11.0) temple (>= 0.8.2) thor tilt @@ -180,13 +169,13 @@ GEM http_parser.rb (0.6.0) i18n (1.7.0) concurrent-ruby (~> 1.0) - image_processing (1.9.3) + image_processing (1.10.0) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.13, < 3) - inline_svg (1.5.2) + inline_svg (1.6.0) activesupport (>= 3.0) nokogiri (>= 1.6) - jaro_winkler (1.5.3) + jaro_winkler (1.5.4) jbuilder (2.9.1) activesupport (>= 4.2.0) jekyll (4.0.0) @@ -204,7 +193,7 @@ GEM rouge (~> 3.0) safe_yaml (~> 1.0) terminal-table (~> 1.8) - jekyll-sass-converter (2.0.0) + jekyll-sass-converter (2.0.1) sassc (> 2.0.1, < 3.0) jekyll-watch (2.2.1) listen (~> 3.0) @@ -220,7 +209,7 @@ GEM rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) ruby_dep (~> 1.2) - loofah (2.3.1) + loofah (2.4.0) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.1) @@ -237,48 +226,45 @@ GEM mobility (0.8.9) i18n (>= 0.6.10, < 2) request_store (~> 1.0) - net-scp (2.0.0) - net-ssh (>= 2.6.5, < 6.0.0) - net-ssh (5.2.0) - netaddr (2.0.3) + netaddr (2.0.4) nio4r (2.5.2) - nokogiri (1.10.5) + nokogiri (1.10.7) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) - parallel (1.17.0) - parser (2.6.4.1) + parallel (1.19.1) + parser (2.7.0.1) ast (~> 2.4.0) pathutil (0.16.2) forwardable-extended (~> 2.6) - pg (1.1.4) + pg (1.2.0) popper_js (1.14.5) pry (0.12.2) coderay (~> 1.1.0) method_source (~> 0.9.0) - public_suffix (4.0.1) - puma (4.3.0) + public_suffix (4.0.2) + puma (4.3.1) nio4r (~> 2.0) pundit (2.1.0) activesupport (>= 3.0.0) - rack (2.0.7) + rack (2.0.8) rack-proxy (0.6.5) rack rack-test (1.1.0) rack (>= 1.0, < 3) - rails (6.0.1) - actioncable (= 6.0.1) - actionmailbox (= 6.0.1) - actionmailer (= 6.0.1) - actionpack (= 6.0.1) - actiontext (= 6.0.1) - actionview (= 6.0.1) - activejob (= 6.0.1) - activemodel (= 6.0.1) - activerecord (= 6.0.1) - activestorage (= 6.0.1) - activesupport (= 6.0.1) + rails (6.0.2.1) + actioncable (= 6.0.2.1) + actionmailbox (= 6.0.2.1) + actionmailer (= 6.0.2.1) + actionpack (= 6.0.2.1) + actiontext (= 6.0.2.1) + actionview (= 6.0.2.1) + activejob (= 6.0.2.1) + activemodel (= 6.0.2.1) + activerecord (= 6.0.2.1) + activestorage (= 6.0.2.1) + activesupport (= 6.0.2.1) bundler (>= 1.3.0) - railties (= 6.0.1) + railties (= 6.0.2.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -290,16 +276,16 @@ GEM railties (>= 6.0.0, < 7) rails_warden (0.6.0) warden (>= 1.2.0) - railties (6.0.1) - actionpack (= 6.0.1) - activesupport (= 6.0.1) + railties (6.0.2.1) + actionpack (= 6.0.2.1) + activesupport (= 6.0.2.1) method_source rake (>= 0.8.7) thor (>= 0.20.3, < 2.0) rainbow (3.0.0) - rake (12.3.3) + rake (13.0.1) rb-fsevent (0.10.3) - rb-inotify (0.10.0) + rb-inotify (0.10.1) ffi (~> 1.0) rbnacl (4.0.2) ffi @@ -311,52 +297,43 @@ GEM redis-activesupport (5.2.0) activesupport (>= 3, < 7) redis-store (>= 1.3, < 2) - redis-rack (2.0.5) + redis-rack (2.0.6) rack (>= 1.5, < 3) redis-store (>= 1.2, < 2) redis-rails (5.0.2) redis-actionpack (>= 5.0, < 6) redis-activesupport (>= 5.0, < 6) redis-store (>= 1.2, < 2) - redis-store (1.6.0) - redis (>= 2.2, < 5) - request_store (1.4.1) + redis-store (1.8.1) + redis (>= 4, < 5) + request_store (1.5.0) rack (>= 1.4) responders (3.0.0) actionpack (>= 5.0) railties (>= 5.0) - rouge (3.11.0) - rubocop (0.74.0) + rouge (3.14.0) + rubocop (0.78.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) parser (>= 2.6) rainbow (>= 2.2.2, < 4.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 1.7) - rubocop-rails (2.3.2) + rubocop-rails (2.4.1) rack (>= 1.1) rubocop (>= 0.72.0) ruby-enum (0.7.2) i18n ruby-progressbar (1.10.1) - ruby-vips (2.0.15) + ruby-vips (2.0.16) ffi (~> 1.9) ruby_dep (1.5.0) - rubyzip (1.3.0) - rugged (0.28.3.1) + rubyzip (2.0.0) + rugged (0.28.4.1) safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sass-rails (5.1.0) - railties (>= 5.2.0) - sass (~> 3.1) - sprockets (>= 2.8, < 4.0) - sprockets-rails (>= 2.0, < 4.0) - tilt (>= 1.1, < 3) - sassc (2.2.0) + safely_block (0.3.0) + errbase (>= 0.1.1) + sassc (2.2.1) ffi (~> 1.9) sassc-rails (2.1.2) railties (>= 4.0.0) @@ -364,41 +341,38 @@ GEM sprockets (> 3.0) sprockets-rails tilt - selenium-webdriver (3.142.4) - childprocess (>= 0.5, < 3.0) - rubyzip (~> 1.2, >= 1.2.2) + selenium-webdriver (3.142.7) + childprocess (>= 0.5, < 4.0) + rubyzip (>= 1.2.2) simpleidn (0.1.1) unf (~> 0.1.4) spring (2.1.0) spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (3.7.2) + sprockets (4.0.0) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.2.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) - sqlite3 (1.4.1) - sshkit (1.20.0) - net-scp (>= 1.1.2) - net-ssh (>= 2.8.0) + sqlite3 (1.4.2) sucker_punch (2.1.2) concurrent-ruby (~> 1.0) sysexits (1.2.0) temple (0.8.2) terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) - thor (0.20.3) + thor (1.0.1) thread_safe (0.3.6) - tilt (2.0.9) - turbolinks (5.2.0) + tilt (2.0.10) + turbolinks (5.2.1) turbolinks-source (~> 5.2) turbolinks-source (5.2.0) - tzinfo (1.2.5) + tzinfo (1.2.6) thread_safe (~> 0.1) - uglifier (4.1.20) + uglifier (4.2.0) execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext @@ -414,7 +388,7 @@ GEM activemodel (>= 6.0.0) bindex (>= 0.4.0) railties (>= 6.0.0) - webpacker (4.0.7) + webpacker (4.2.2) activesupport (>= 4.2) rack-proxy (>= 0.6.1) railties (>= 4.2) @@ -423,7 +397,7 @@ GEM websocket-extensions (0.1.4) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.2.1) + zeitwerk (2.2.2) PLATFORMS ruby @@ -431,12 +405,9 @@ PLATFORMS DEPENDENCIES bcrypt (~> 3.1.7) bcrypt_pbkdf + blazer bootstrap (~> 4) - capistrano - capistrano-bundler - capistrano-passenger - capistrano-rails - capistrano-rbenv + brakeman capybara (~> 2.13) commonmarker database_cleaner @@ -474,7 +445,7 @@ DEPENDENCIES rubocop-rails rubyzip rugged - sass-rails (~> 5.0) + sassc-rails selenium-webdriver spring spring-watcher-listen (~> 2.0.0) @@ -489,7 +460,7 @@ DEPENDENCIES yaml_db! RUBY VERSION - ruby 2.5.7p206 + ruby 2.6.5p114 BUNDLED WITH - 2.0.2 + 2.1.4 diff --git a/Makefile b/Makefile index 226328d2..78154240 100644 --- a/Makefile +++ b/Makefile @@ -3,6 +3,9 @@ mkfile_path := $(abspath $(lastword $(MAKEFILE_LIST))) root_dir := $(patsubst %/,%,$(dir $(mkfile_path))) include $(root_dir)/.env +serve: + bundle exec rails s -b "ssl://0.0.0.0:3000?key=config/sutty.local.key&cert=config/sutty.local.crt" + # Limpiar los archivos de testeo clean: rm -rf _sites/test-* _deploy/test-* @@ -11,13 +14,22 @@ clean: build: docker build --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/sutty . +save: + docker save sutty/sutty:latest | gzip | ssh root@sutty.nl docker load + +load: + ssh root@sutty.nl sh -c "gunzip -c sutty.latest.gz | docker load" + # Crear el directorio donde se almacenan las gemas binarias ../gems/: mkdir -p $@ +ifeq ($(MAKECMDGOALS),convert-gems) gem_dir := $(shell readlink -f ../gems) -gems := $(shell bundle show --paths | xargs -I {} sh -c 'test -d {}/ext && basename {}') +gems := $(shell bundle show --paths | xargs -I {} sh -c 'test -f {}/ext/*/extconf.rb && basename {}') +gems += $(shell bundle show --paths | xargs -I {} sh -c 'test -f {}/ext/extconf.rb && basename {}') gems_musl := $(patsubst %,$(gem_dir)/%-x86_64-linux-musl.gem,$(gems)) +endif $(gem_dir)/%-x86_64-linux-musl.gem: docker run \ @@ -45,8 +57,8 @@ test-container: $(dirs) -v $(PWD)/root/public:/srv/http/_public \ -v $(PWD)/config/credentials.yml.enc:/srv/http/config/credentials.yml.enc \ -e RAILS_MASTER_KEY=`cat config/master.key` \ + -e RAILS_ENV=production \ -it \ --rm \ --name=sutty \ - sutty/sutty \ - /bin/sh + sutty/sutty /bin/sh diff --git a/app/assets/fonts/saira/v3/Saira-subset.woff2 b/app/assets/fonts/saira/v3/Saira-subset.woff2 new file mode 100644 index 00000000..17444074 Binary files /dev/null and b/app/assets/fonts/saira/v3/Saira-subset.woff2 differ diff --git a/app/assets/fonts/saira/v3/Saira-subset.zopfli.woff b/app/assets/fonts/saira/v3/Saira-subset.zopfli.woff new file mode 100644 index 00000000..28712cf2 Binary files /dev/null and b/app/assets/fonts/saira/v3/Saira-subset.zopfli.woff differ diff --git a/app/assets/fonts/saira/v3/SairaBold-subset.woff2 b/app/assets/fonts/saira/v3/SairaBold-subset.woff2 new file mode 100644 index 00000000..1cdf264e Binary files /dev/null and b/app/assets/fonts/saira/v3/SairaBold-subset.woff2 differ diff --git a/app/assets/fonts/saira/v3/SairaBold-subset.zopfli.woff b/app/assets/fonts/saira/v3/SairaBold-subset.zopfli.woff new file mode 100644 index 00000000..772ee88b Binary files /dev/null and b/app/assets/fonts/saira/v3/SairaBold-subset.zopfli.woff differ diff --git a/app/assets/fonts/saira/v3/SairaMedium-subset.woff2 b/app/assets/fonts/saira/v3/SairaMedium-subset.woff2 new file mode 100644 index 00000000..9a95bcd0 Binary files /dev/null and b/app/assets/fonts/saira/v3/SairaMedium-subset.woff2 differ diff --git a/app/assets/fonts/saira/v3/SairaMedium-subset.zopfli.woff b/app/assets/fonts/saira/v3/SairaMedium-subset.zopfli.woff new file mode 100644 index 00000000..061310a2 Binary files /dev/null and b/app/assets/fonts/saira/v3/SairaMedium-subset.zopfli.woff differ diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 5585413d..b3a88447 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -1,5 +1,6 @@ //= require rails-ujs //= require turbolinks //= require input-tag/input-tag.js +//= require input-map/input-map.js //= require zepto/dist/zepto.min.js //= require_tree . diff --git a/app/assets/javascripts/input-tag.js b/app/assets/javascripts/input-tag.js index ed952c49..99079891 100644 --- a/app/assets/javascripts/input-tag.js +++ b/app/assets/javascripts/input-tag.js @@ -7,4 +7,13 @@ $(document).on('turbolinks:load', function() { props: { ...this.dataset } }); }); + + $('.mapable').each(function() { + this.innerHTML = ''; + + new InputMap({ + target: this, + props: { ...this.dataset } + }); + }); }); diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index 2ce28375..9b05f0ab 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -20,6 +20,18 @@ $magenta: #f206f9; --background: #{$black}; --color: #{$cyan}; } + trix-toolbar { + .trix-button--icon { + background-color: var(--color); + } + } +} + +trix-toolbar { + background-color: var(--background); + position: sticky; + top: 0; + } // TODO: Encontrar la forma de generar esto desde los locales de Rails @@ -34,7 +46,8 @@ $custom-file-text: ( font-weight: 500; font-display: optional; src: local('Saira Medium'), local('Saira-Medium'), - font-url('saira/v3/SairaMedium.ttf') format('truetype'); + font-url('saira/v3/SairaMedium-subset.woff2') format('woff2'), + font-url('saira/v3/SairaMedium-subset.zopfli.woff') format('woff'); } @font-face { @@ -43,7 +56,8 @@ $custom-file-text: ( font-weight: 700; font-display: optional; src: local('Saira Bold'), local('Saira-Bold'), - font-url('saira/v3/SairaBold.ttf') format('truetype'); + font-url('saira/v3/SairaBold-subset.woff2') format('woff2'), + font-url('saira/v3/SairaBold-subset.zopfli.woff') format('woff'); } body { @@ -128,7 +142,9 @@ ol.breadcrumb { transition: all 3s; } +.mapable, .taggable { + .input-map, .input-tag { legend { @extend .sr-only @@ -144,7 +160,7 @@ ol.breadcrumb { &[type=text] { @extend .form-control; display: inline-block; - width: calc(100% - 90px); + width: calc(100% - 93px); } &[type=checkbox] { @@ -169,6 +185,8 @@ svg { color: var(--background); border: none; border-radius: 0; + margin-right: 0.3rem; + margin-bottom: 0.3rem; &:hover { color: var(--background); diff --git a/app/controllers/api/v1/base_controller.rb b/app/controllers/api/v1/base_controller.rb index 517fa28f..6f76e51a 100644 --- a/app/controllers/api/v1/base_controller.rb +++ b/app/controllers/api/v1/base_controller.rb @@ -4,9 +4,6 @@ module Api module V1 # API class BaseController < ActionController::Base - http_basic_authenticate_with name: ENV['HTTP_BASIC_USER'], - password: ENV['HTTP_BASIC_PASSWORD'] - protect_from_forgery with: :null_session respond_to :json end diff --git a/app/controllers/api/v1/csp_reports_controller.rb b/app/controllers/api/v1/csp_reports_controller.rb new file mode 100644 index 00000000..cdce92d6 --- /dev/null +++ b/app/controllers/api/v1/csp_reports_controller.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +module Api + module V1 + # Recibe los reportes de Content Security Policy + class CspReportsController < BaseController + # Crea un reporte de CSP intercambiando los guiones medios por + # bajos + # + # TODO: Aplicar rate_limit + def create + csp = CspReport.new(csp_report_params.to_h.map do |k, v| + { k.tr('-', '_') => v } + end.inject(&:merge)) + + csp.id = SecureRandom.uuid + csp.save + + render json: {}, status: :created + end + + private + + # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only#Violation_report_syntax + def csp_report_params + params.require(:'csp-report') + .permit(:disposition, + :referrer, + :'blocked-uri', + :'document-uri', + :'effective-directive', + :'original-policy', + :'script-sample', + :'status-code', + :'violated-directive', + :'line-number', + :'column-number', + :'source-file') + end + end + end +end diff --git a/app/controllers/api/v1/sites_controller.rb b/app/controllers/api/v1/sites_controller.rb index 8bea164e..fcf694e0 100644 --- a/app/controllers/api/v1/sites_controller.rb +++ b/app/controllers/api/v1/sites_controller.rb @@ -4,8 +4,12 @@ module Api module V1 # API para sitios class SitesController < BaseController + http_basic_authenticate_with name: ENV['HTTP_BASIC_USER'], + password: ENV['HTTP_BASIC_PASSWORD'] + def index - render json: Site.all.order(:name).pluck(:name) + render json: Site.all.order(:name).pluck(:name) + + DeployAlternativeDomain.all.map(&:hostname) end # Detecta si se puede generar un certificado diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 14af15fa..4d999d9a 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -12,7 +12,8 @@ class PostsController < ApplicationController @layout = params.dig(:layout).try :to_sym # TODO: Aplicar policy_scope @posts = @site.posts(lang: I18n.locale) - @posts.sort_by! :order, :date + @posts.sort_by!(:order, :date).reverse! + @usuarie = @site.usuarie? current_usuarie end def show @@ -34,7 +35,7 @@ class PostsController < ApplicationController usuarie: current_usuarie, params: params) - if service.create.persisted? + if (@post = service.create.persisted?) redirect_to site_posts_path(@site) else render 'posts/new' diff --git a/app/controllers/sites_controller.rb b/app/controllers/sites_controller.rb index c770e092..598c7755 100644 --- a/app/controllers/sites_controller.rb +++ b/app/controllers/sites_controller.rb @@ -8,7 +8,7 @@ class SitesController < ApplicationController # Ver un listado de sitios def index authorize Site - @sites = current_usuarie.sites + @sites = current_usuarie.sites.order(:title) end # No tenemos propiedades de un sitio aún, así que vamos al listado de diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index f0f75a9e..f557aa36 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -186,7 +186,11 @@ document.addEventListener('turbolinks:load', e => { onlyBody: true, dragHandler: '.handle', }).on('drop', (from, to, el, mode) => { - $('.reorder').val((i, v) => i) - $('.submit-reorder').removeClass('d-none') + Array.from(document.querySelectorAll('.reorder')) + .reverse() + .map((o,i) => o.value = i) + + Array.from(document.querySelectorAll('.submit-reorder')) + .map(s => s.classList.remove('d-none')) }) }) diff --git a/app/jobs/deploy_job.rb b/app/jobs/deploy_job.rb index 608b485b..85411cb0 100644 --- a/app/jobs/deploy_job.rb +++ b/app/jobs/deploy_job.rb @@ -15,6 +15,9 @@ class DeployJob < ApplicationJob # No es opcional unless @deployed[:deploy_local] @site.update_attribute :status, 'waiting' + notify_usuaries + + # Hacer fallar la tarea raise DeployException, deploy_local.build_stats.last.log end diff --git a/app/models/csp_report.rb b/app/models/csp_report.rb new file mode 100644 index 00000000..e76c23bb --- /dev/null +++ b/app/models/csp_report.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Almacena un reporte de CSP +class CspReport < ApplicationRecord; end diff --git a/app/models/deploy_alternative_domain.rb b/app/models/deploy_alternative_domain.rb new file mode 100644 index 00000000..e4960e65 --- /dev/null +++ b/app/models/deploy_alternative_domain.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Soportar dominios alternativos +class DeployAlternativeDomain < Deploy + store :values, accessors: %i[hostname], coder: JSON + + # Generar un link simbólico del sitio principal al alternativo + def deploy + File.symlink?(destination) || + File.symlink(site.hostname, destination).zero? + end + + # No hay límite para los dominios alternativos + def limit; end + + def size + File.size destination + end + + def destination + File.join(Rails.root, '_deploy', hostname.gsub(/\.\z/, '')) + end +end diff --git a/app/models/deploy_local.rb b/app/models/deploy_local.rb index 9f143bd3..31e147ad 100644 --- a/app/models/deploy_local.rb +++ b/app/models/deploy_local.rb @@ -13,7 +13,11 @@ class DeployLocal < Deploy # Pasamos variables de entorno mínimas para no filtrar secretos de # Sutty def deploy - mkdir && yarn && bundle && jekyll_build + return false unless mkdir + return false unless yarn + return false unless bundle + + jekyll_build end # Sólo permitimos un deploy local @@ -49,7 +53,8 @@ class DeployLocal < Deploy { 'HOME' => home_dir, 'PATH' => paths.join(':'), - 'JEKYLL_ENV' => Rails.env + 'JEKYLL_ENV' => Rails.env, + 'LANG' => ENV['LANG'] } end @@ -61,6 +66,10 @@ class DeployLocal < Deploy File.exist? yarn_lock end + def gem + run %(gem install bundler --no-document) + end + # Corre yarn dentro del repositorio def yarn return unless yarn_lock? diff --git a/app/models/metadata_color.rb b/app/models/metadata_color.rb new file mode 100644 index 00000000..450700ea --- /dev/null +++ b/app/models/metadata_color.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# Un campo de correo +class MetadataColor < MetadataString; end diff --git a/app/models/metadata_email.rb b/app/models/metadata_email.rb new file mode 100644 index 00000000..bbe95561 --- /dev/null +++ b/app/models/metadata_email.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Un campo de correo +# TODO: Validar que tenga un formato correo +class MetadataEmail < MetadataString; end diff --git a/app/models/metadata_geo.rb b/app/models/metadata_geo.rb new file mode 100644 index 00000000..3b7c31bd --- /dev/null +++ b/app/models/metadata_geo.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Define un campo de coordenadas geográficas +class MetadataGeo < MetadataTemplate + def default_value + { 'lat' => nil, 'lng' => nil } + end + + def empty? + value == default_value + end + + def to_param + { name => %i[lat lng] } + end + + def save + self[:value] = { + 'lat' => self[:value]['lat'].to_f, + 'lng' => self[:value]['lng'].to_f + } + end +end diff --git a/app/models/metadata_order.rb b/app/models/metadata_order.rb index 0b696ed8..e1f66e68 100644 --- a/app/models/metadata_order.rb +++ b/app/models/metadata_order.rb @@ -2,9 +2,9 @@ # Un campo de orden class MetadataOrder < MetadataTemplate - # El valor según la posición del post en la relación, siguiendo el - # orden cronológico inverso + # El valor según la posición del post en la relación ordenada por + # fecha, a fecha más alta, posición más alta def default_value - site.posts(lang: post.lang.value).index(post) + site.posts(lang: post.lang.value).sort_by(:date).index(post) end end diff --git a/app/models/metadata_related_posts.rb b/app/models/metadata_related_posts.rb new file mode 100644 index 00000000..7d2273a0 --- /dev/null +++ b/app/models/metadata_related_posts.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Devuelve una lista de títulos y UUID de todos los posts del mismo +# idioma que el actual, para usar con input-map.js +class MetadataRelatedPosts < MetadataArray + # Genera un Hash de { title | slug => uuid } + def values + site.posts(lang: lang).map do |p| + { title(p) => p.uuid.value } + end.inject(:merge) + end + + private + + def title(post) + post.try(:title).try(:value) || post.try(:slug).try(:value) + end + + # TODO: Traer el idioma actual de otra forma + def lang + post.try(:lang).try(:value) || I18n.locale + end +end diff --git a/app/models/metadata_template.rb b/app/models/metadata_template.rb index de5007b9..fb8e0388 100644 --- a/app/models/metadata_template.rb +++ b/app/models/metadata_template.rb @@ -15,6 +15,8 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type, # Valores posibles, busca todos los valores actuales en otros # artículos del mismo sitio + # + # TODO: Implementar lang! def values site.everything_of(name) end diff --git a/app/models/metadata_uuid.rb b/app/models/metadata_uuid.rb new file mode 100644 index 00000000..ba51c708 --- /dev/null +++ b/app/models/metadata_uuid.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# Asigna un identificador único al artículo +class MetadataUuid < MetadataTemplate + def default_value + SecureRandom.uuid + end +end diff --git a/app/models/post.rb b/app/models/post.rb index 0dd9299f..b0c6ded6 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'jekyll/utils' - # Esta clase representa un post en un sitio jekyll e incluye métodos # para modificarlos y crear nuevos. # @@ -13,7 +11,16 @@ class Post < OpenStruct DEFAULT_ATTRIBUTES = %i[site document layout].freeze # Otros atributos que no vienen en los metadatos PRIVATE_ATTRIBUTES = %i[path slug attributes errors].freeze - PUBLIC_ATTRIBUTES = %i[lang date].freeze + PUBLIC_ATTRIBUTES = %i[lang date uuid].freeze + + class << self + # Obtiene el layout sin leer el Document + def find_layout(doc) + SafeYAML.load(IO.foreach(doc.path).lazy.grep(/^layout: /).take(1).first) + .try(:[], 'layout') + .try(:to_sym) + end + end # Redefinir el inicializador de OpenStruct # @@ -33,6 +40,10 @@ class Post < OpenStruct # MetadataFactory devuelve un tipo de campo por cada campo. A # partir de ahí se pueden obtener los valores actuales y una lista # de valores por defecto. + # + # XXX: En el primer intento de hacerlo más óptimo, movimos esta + # lógica a instanciación bajo demanda, pero no solo no logramos + # optimizar sino que aumentamos el tiempo de carga :/ layout.metadata.each_pair do |name, template| send "#{name}=".to_sym, MetadataFactory.build(document: document, @@ -47,31 +58,27 @@ class Post < OpenStruct required: template['required']) end + # TODO: Llamar dinámicamente load_lang! load_slug! load_date! load_path! + load_uuid! - # Leer el documento - read + # XXX: No usamos Post#read porque a esta altura todavía no sabemos + # nada del Document + document.read! if File.exist? document.path end + # TODO: Convertir a UUID? def id path.basename end - def sha1 - Digest::SHA1.hexdigest id + def updated_at + File.mtime(path.absolute) end - # Levanta un error si al construir el artículo no pasamos un atributo. - def default_attributes_missing(**args) - DEFAULT_ATTRIBUTES.each do |attr| - i18n = I18n.t("exceptions.post.#{attr}_missing") - - raise ArgumentError, i18n unless args[attr].present? - end - end # Solo ejecuta la magia de OpenStruct si el campo existe en la # plantilla @@ -82,14 +89,18 @@ class Post < OpenStruct # XXX: rubocop dice que tenemos que usar super cuando ya lo estamos # usando... def method_missing(mid, *args) - unless attribute? mid + # Limpiar el nombre del atributo, para que todos los ayudantes + # reciban el método en limpio + name = attribute_name mid + + unless attribute? name raise NoMethodError, I18n.t('exceptions.post.no_method', method: mid) end # Definir los attribute_* - new_attribute_was(mid) - new_attribute_changed(mid) + new_attribute_was(name) + new_attribute_changed(name) # OpenStruct super(mid, *args) @@ -101,12 +112,15 @@ class Post < OpenStruct # Detecta si es un atributo válido o no, a partir de la tabla de la # plantilla def attribute?(mid) - attrs = DEFAULT_ATTRIBUTES + PRIVATE_ATTRIBUTES + PUBLIC_ATTRIBUTES - if singleton_class.method_defined? :attributes - (attrs + attributes).include? attribute_name(mid) - else - attrs.include? attribute_name(mid) + included = DEFAULT_ATTRIBUTES.include?(mid) || + PRIVATE_ATTRIBUTES.include?(mid) || + PUBLIC_ATTRIBUTES.include?(mid) + + if !included && singleton_class.method_defined?(:attributes) + included = attributes.include? mid end + + included end # Devuelve los strong params para el layout @@ -133,8 +147,10 @@ class Post < OpenStruct { metadata.to_s => template.value } end.compact.inject(:merge) + # TODO: Convertir a Metadata? # Asegurarse que haya un layout yaml['layout'] = layout.name.to_s + yaml['uuid'] = uuid.value # Y que no se procese liquid yaml['liquid'] = false yaml['usuaries'] = usuaries.map(&:id).uniq @@ -156,8 +172,8 @@ class Post < OpenStruct # Guarda los cambios # rubocop:disable Metrics/CyclomaticComplexity - def save(validation = true) - return false if validation && !valid? + def save(validate: true) + return false if validate && !valid? # Salir si tenemos que cambiar el nombre del archivo y no pudimos return false if !new? && path_changed? && !update_path! return false unless save_attributes! @@ -176,7 +192,7 @@ class Post < OpenStruct return unless written? document.path = path.absolute - document.read + document.read! end def new? @@ -197,12 +213,6 @@ class Post < OpenStruct # Detecta si el artículo es válido para guardar def valid? - validate - errors.blank? - end - - # Requisitos para que el post sea válido - def validate self.errors = {} layout.metadata.keys.map(&:to_sym).each do |metadata| @@ -210,8 +220,9 @@ class Post < OpenStruct errors[metadata] = template.errors unless template.valid? end + + errors.blank? end - alias validate! validate # Guarda los cambios en el archivo destino def write @@ -239,18 +250,35 @@ class Post < OpenStruct end alias update update_attributes + # El Document guarda un Array de los ids de Usuarie. Si está vacío, + # no hacemos una consulta vacía. Si no, traemos todes les Usuaries + # por su id y convertimos a Array para poder agregar o quitar luego + # sin pasar por ActiveRecord. def usuaries - @usuaries ||= Usuarie.where(id: document_usuaries).to_a + @usuaries ||= if (d = document_usuaries).empty? + [] + else + Usuarie.where(id: d).to_a + end end private + # Levanta un error si al construir el artículo no pasamos un atributo. + def default_attributes_missing(**args) + DEFAULT_ATTRIBUTES.each do |attr| + i18n = I18n.t("exceptions.post.#{attr}_missing") + + raise ArgumentError, i18n unless args[attr].present? + end + end + def document_usuaries document.data.fetch('usuaries', []) end def new_attribute_was(method) - attr_was = (attribute_name(method).to_s + '_was').to_sym + attr_was = "#{method}_was".to_sym return attr_was if singleton_class.method_defined? attr_was define_singleton_method(attr_was) do @@ -265,7 +293,7 @@ class Post < OpenStruct # Pregunta si el atributo cambió def new_attribute_changed(method) - attr_changed = (attribute_name(method).to_s + '_changed?').to_sym + attr_changed = "#{method}_changed?".to_sym return attr_changed if singleton_class.method_defined? attr_changed @@ -314,6 +342,13 @@ class Post < OpenStruct required: true) end + def load_uuid! + self.uuid = MetadataUuid.new(document: document, site: site, + layout: layout, name: :uuid, + type: :uuid, post: self, + required: true) + end + # Ejecuta la acción de guardado en cada atributo def save_attributes! attributes.map do |attr| diff --git a/app/models/post_relation.rb b/app/models/post_relation.rb index 22eb5ca9..e262893d 100644 --- a/app/models/post_relation.rb +++ b/app/models/post_relation.rb @@ -31,15 +31,18 @@ class PostRelation < Array post end + alias sort_by_generic sort_by alias sort_by_generic! sort_by! # Permite ordenar los artículos por sus atributos # # XXX: Prestar atención cuando estamos mezclando artículos con # diferentes tipos de atributos. - def sort_by!(*attrs) - sort_by_generic! do |post| + def sort_by(*attrs) + sort_by_generic do |post| attrs.map do |attr| + # TODO: detectar el tipo de atributo faltante y obtener el valor + # por defecto para hacer la comparación return 0 unless post.attributes.include? attr post.public_send(attr).value @@ -47,12 +50,20 @@ class PostRelation < Array end end + def sort_by!(*attrs) + replace sort_by(*attrs) + end + alias find_generic find - # Encontra un post por su id convertido a SHA1 - def find(id, sha1: false) + # Encontrar un post por su UUID + def find(id, uuid: false) find_generic do |p| - p.sha1 == (sha1 ? id : Digest::SHA1.hexdigest(id)) + if uuid + p.uuid.value == id + else + p.id == id + end end end @@ -65,8 +76,10 @@ class PostRelation < Array end # Intenta guardar todos y devuelve true si pudo - def save_all - map(&:save).all? + def save_all(validate: true) + map do |post| + post.save(validate: validate) + end.all? end private diff --git a/app/models/site.rb b/app/models/site.rb index d81b95f9..9d734247 100644 --- a/app/models/site.rb +++ b/app/models/site.rb @@ -9,7 +9,10 @@ class Site < ApplicationRecord # @see app/services/site_service.rb DEPLOYS = %i[local www zip].freeze - validates :name, uniqueness: true, hostname: true + validates :name, uniqueness: true, hostname: { + allow_root_label: true + } + validates :design_id, presence: true validate :deploy_local_presence validates_inclusion_of :status, in: %w[waiting enqueued building] @@ -173,14 +176,10 @@ class Site < ApplicationRecord @posts[lang] = PostRelation.new site: self - # Jekyll lee los documentos en orden cronológico pero los invierte - # durante la renderización. Usamos el orden cronológico inverso por - # defecto para mostrar los artículos más nuevos primero. - docs = collections[lang.to_s].try(:docs).try(:sort) { |a, b| b <=> a } # No fallar si no existe colección para este idioma # XXX: queremos fallar silenciosamente? - (docs || []).each do |doc| - layout = layouts[doc.data['layout'].to_sym] + (collections[lang.to_s].try(:docs) || []).each do |doc| + layout = layouts[Post.find_layout(doc)] @posts[lang].build(document: doc, layout: layout, lang: lang) end diff --git a/app/models/site/static_file_migration.rb b/app/models/site/static_file_migration.rb index 4d08fc8c..48b3cf28 100644 --- a/app/models/site/static_file_migration.rb +++ b/app/models/site/static_file_migration.rb @@ -79,7 +79,7 @@ class Site end # Guardamos los cambios - unless doc.save(false) + unless doc.save(validate: false) log.write "#{doc.path.relative} no se pudo guardar\n" end diff --git a/app/services/post_service.rb b/app/services/post_service.rb index 768c1fdd..85a40ee8 100644 --- a/app/services/post_service.rb +++ b/app/services/post_service.rb @@ -8,8 +8,8 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do # # @return Post def create - # TODO: Implementar layout - self.post = site.posts(lang: params[:post][:lang] || I18n.locale).build + self.post = site.posts(lang: params[:post][:lang] || I18n.locale) + .build(layout: params[:post][:layout]) post.usuaries << usuarie params[:post][:draft] = true if site.invitade? usuarie @@ -40,25 +40,31 @@ PostService = Struct.new(:site, :usuarie, :post, :params, keyword_init: true) do end # Reordena todos los posts que soporten orden de acuerdo a un hash de - # ids y nuevas posiciones. La posición actual la da la posición en + # uuids y nuevas posiciones. La posición actual la da la posición en # el array. # - # { sha1 => 2, sha1 => 1, sha1 => 0 } + # { uuid => 2, uuid => 1, uuid => 0 } def reorder posts = site.posts(lang: lang) reorder = params.require(:post).permit(reorder: {}).try(:[], :reorder) + modified = PostRelation.new(site: site) - files = reorder.keys.map do |id| - post = posts.find(id, sha1: true) + files = reorder.keys.map do |uuid| + post = posts.find(uuid, uuid: true) + order = reorder[uuid].to_i + + next unless post next unless post.attributes.include? :order + next if post.order.value == order - post.usuaries << usuarie - post.order.value = reorder[id].to_i + modified << post + post.order.value = order post.path.absolute end.compact # TODO: Implementar transacciones! - posts.save_all && commit(action: :reorder, file: files) + modified.save_all(validate: false) && + commit(action: :reorder, file: files) end private diff --git a/app/views/devise/mailer/confirmation_instructions.haml b/app/views/devise/mailer/confirmation_instructions.html.haml similarity index 100% rename from app/views/devise/mailer/confirmation_instructions.haml rename to app/views/devise/mailer/confirmation_instructions.html.haml diff --git a/app/views/devise/mailer/confirmation_instructions.text.haml b/app/views/devise/mailer/confirmation_instructions.text.haml new file mode 100644 index 00000000..38e4c548 --- /dev/null +++ b/app/views/devise/mailer/confirmation_instructions.text.haml @@ -0,0 +1,5 @@ += t('.greeting', recipient: @email) +\ += t('.instruction') +\ += confirmation_url(@resource, confirmation_token: @token) diff --git a/app/views/devise/mailer/email_changed.haml b/app/views/devise/mailer/email_changed.html.haml similarity index 100% rename from app/views/devise/mailer/email_changed.haml rename to app/views/devise/mailer/email_changed.html.haml diff --git a/app/views/devise/mailer/email_changed.text.haml b/app/views/devise/mailer/email_changed.text.haml new file mode 100644 index 00000000..e5216a5a --- /dev/null +++ b/app/views/devise/mailer/email_changed.text.haml @@ -0,0 +1,6 @@ += t('.greeting', recipient: @email) +\ +- if @resource.try(:unconfirmed_email?) + = t('.message', email: @resource.unconfirmed_email) +- else + = t('.message', email: @resource.email) diff --git a/app/views/devise/mailer/invitation_instructions.haml b/app/views/devise/mailer/invitation_instructions.html.haml similarity index 100% rename from app/views/devise/mailer/invitation_instructions.haml rename to app/views/devise/mailer/invitation_instructions.html.haml diff --git a/app/views/devise/mailer/password_change.haml b/app/views/devise/mailer/password_change.html.haml similarity index 100% rename from app/views/devise/mailer/password_change.haml rename to app/views/devise/mailer/password_change.html.haml diff --git a/app/views/devise/mailer/password_change.text.haml b/app/views/devise/mailer/password_change.text.haml new file mode 100644 index 00000000..fe4c1ea5 --- /dev/null +++ b/app/views/devise/mailer/password_change.text.haml @@ -0,0 +1,3 @@ += t('.greeting', recipient: @resource.email) +\ += t('.message') diff --git a/app/views/devise/mailer/reset_password_instructions.haml b/app/views/devise/mailer/reset_password_instructions.html.haml similarity index 100% rename from app/views/devise/mailer/reset_password_instructions.haml rename to app/views/devise/mailer/reset_password_instructions.html.haml diff --git a/app/views/devise/mailer/reset_password_instructions.text.haml b/app/views/devise/mailer/reset_password_instructions.text.haml new file mode 100644 index 00000000..3d0fe64d --- /dev/null +++ b/app/views/devise/mailer/reset_password_instructions.text.haml @@ -0,0 +1,9 @@ += t('.greeting', recipient: @resource.email) +\ += t('.instruction') +\ += edit_password_url(@resource, reset_password_token: @token) +\ += t('.instruction_2') +\ += t('.instruction_3') diff --git a/app/views/devise/mailer/unlock_instructions.haml b/app/views/devise/mailer/unlock_instructions.html.haml similarity index 100% rename from app/views/devise/mailer/unlock_instructions.haml rename to app/views/devise/mailer/unlock_instructions.html.haml diff --git a/app/views/devise/mailer/unlock_instructions.text.haml b/app/views/devise/mailer/unlock_instructions.text.haml new file mode 100644 index 00000000..cf06927b --- /dev/null +++ b/app/views/devise/mailer/unlock_instructions.text.haml @@ -0,0 +1,7 @@ += t('.greeting', recipient: @resource.email) +\ += t('.message') +\ += t('.instruction') +\ += unlock_url(@resource, unlock_token: @token) diff --git a/app/views/devise/registrations/new.haml b/app/views/devise/registrations/new.haml index 41d2fbfa..92a44aec 100644 --- a/app/views/devise/registrations/new.haml +++ b/app/views/devise/registrations/new.haml @@ -5,9 +5,8 @@ .row.align-items-center.justify-content-center.full-height .col-md-5.align-self-center - .sr-only - %h2= t('.sign_up') - %p= t('.help') + %h2= t('.sign_up') + %p= t('.help') = form_for(resource, as: resource_name, diff --git a/app/views/devise/shared/_links.haml b/app/views/devise/shared/_links.haml index 0e646e41..c182d323 100644 --- a/app/views/devise/shared/_links.haml +++ b/app/views/devise/shared/_links.haml @@ -5,7 +5,8 @@ %br/ - if devise_mapping.registerable? && controller_name != 'registrations' - = link_to t('.sign_up'), new_registration_path(resource_name) + = link_to t('.sign_up'), new_registration_path(resource_name), + class: 'btn btn-lg btn-block btn-success' %br/ - if devise_mapping.recoverable? diff --git a/app/views/layouts/_breadcrumb.haml b/app/views/layouts/_breadcrumb.haml index f94e329c..3551153a 100644 --- a/app/views/layouts/_breadcrumb.haml +++ b/app/views/layouts/_breadcrumb.haml @@ -1,6 +1,6 @@ %nav.navbar %a.navbar-brand.d-none.d-sm-block{ href: '/' } - = inline_svg 'sutty.svg', class: 'black', aria: true, + = inline_svg_tag 'sutty.svg', class: 'black', aria: true, title: t('svg.sutty.title'), desc: t('svg.sutty.desc') - if crumbs @@ -21,9 +21,6 @@ - if current_usuarie %ul.navbar-nav - %li.nav-item - = link_to t('.mutual_aid'), mutual_aid_url(local_channel), - class: 'btn' %li.nav-item = link_to t('.logout'), destroy_usuarie_session_path, method: :delete, role: 'button', class: 'btn' diff --git a/app/views/layouts/mailer.text.haml b/app/views/layouts/mailer.text.haml index 36d87bd3..7edd2beb 100644 --- a/app/views/layouts/mailer.text.haml +++ b/app/views/layouts/mailer.text.haml @@ -1,3 +1,4 @@ = yield - +\ += '-- ' = t('.signature') diff --git a/app/views/posts/_form.haml b/app/views/posts/_form.haml index 09136f3e..573389fb 100644 --- a/app/views/posts/_form.haml +++ b/app/views/posts/_form.haml @@ -19,6 +19,8 @@ -# Botones de guardado = render 'posts/submit', site: site, post: post + = hidden_field_tag 'post[layout]', params[:layout] || 'post' + -# Dibuja cada atributo - post.attributes.each do |attribute| - metadata = post.send(attribute) diff --git a/app/views/posts/attribute_ro/_color.haml b/app/views/posts/attribute_ro/_color.haml new file mode 100644 index 00000000..787aacd5 --- /dev/null +++ b/app/views/posts/attribute_ro/_color.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td{ style: "background-color: #{metadata.value}" } = metadata.value diff --git a/app/views/posts/attribute_ro/_content.haml b/app/views/posts/attribute_ro/_content.haml index 27e599d4..31dd8f0d 100644 --- a/app/views/posts/attribute_ro/_content.haml +++ b/app/views/posts/attribute_ro/_content.haml @@ -1,3 +1,3 @@ %tr{ id: attribute } %th= post_label_t(attribute, post: post) - %td= sanitize_markdown metadata.value, tags: tags + %td= metadata.value diff --git a/app/views/posts/attribute_ro/_email.haml b/app/views/posts/attribute_ro/_email.haml new file mode 100644 index 00000000..cf15531c --- /dev/null +++ b/app/views/posts/attribute_ro/_email.haml @@ -0,0 +1,4 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + %a{ href: "mailto:#{metadata.value}" }= metadata.value diff --git a/app/views/posts/attribute_ro/_file.haml b/app/views/posts/attribute_ro/_file.haml index 55c086f5..7bc7b154 100644 --- a/app/views/posts/attribute_ro/_file.haml +++ b/app/views/posts/attribute_ro/_file.haml @@ -2,6 +2,5 @@ %th= post_label_t(attribute, :path, post: post) %td - if metadata.value['path'].present? - %figure - = link_to url_for(metadata.static_file) - %figcaption= metadata.value['description'] + = link_to t('.download'), url_for(metadata.static_file) + %p= metadata.value['description'] diff --git a/app/views/posts/attribute_ro/_geo.haml b/app/views/posts/attribute_ro/_geo.haml new file mode 100644 index 00000000..feeacf25 --- /dev/null +++ b/app/views/posts/attribute_ro/_geo.haml @@ -0,0 +1,9 @@ +- lat = metadata.value['lat'] +- lng = metadata.value['lng'] +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + = link_to t('posts.attributes.geo.uri'), "geo:#{lat},#{lng}" + %br/ + = link_to t('posts.attributes.geo.osm'), + "https://www.openstreetmap.org/?mlat=#{lat}&mlon=#{lng}#map=9/#{lat}/#{lng}" diff --git a/app/views/posts/attribute_ro/_related_posts.haml b/app/views/posts/attribute_ro/_related_posts.haml new file mode 100644 index 00000000..f8ffb73c --- /dev/null +++ b/app/views/posts/attribute_ro/_related_posts.haml @@ -0,0 +1,11 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td + %ul + - metadata.value.each do |v| + - p = site.posts(lang: post.lang.value).find(v, uuid: true) + -# + XXX: Ignorar todos los posts no encontrados (ej: fueron + borrados o el uuid cambió) + - next unless p + %li= link_to p.title.value, site_post_path(site, p.id) diff --git a/app/views/posts/attribute_ro/_uuid.haml b/app/views/posts/attribute_ro/_uuid.haml new file mode 100644 index 00000000..31dd8f0d --- /dev/null +++ b/app/views/posts/attribute_ro/_uuid.haml @@ -0,0 +1,3 @@ +%tr{ id: attribute } + %th= post_label_t(attribute, post: post) + %td= metadata.value diff --git a/app/views/posts/attributes/_color.haml b/app/views/posts/attributes/_color.haml new file mode 100644 index 00000000..a2945016 --- /dev/null +++ b/app/views/posts/attributes/_color.haml @@ -0,0 +1,6 @@ +.form-group + = label_tag "post_#{attribute}", post_label_t(attribute, post: post) + = color_field 'post', attribute, value: metadata.value, + **field_options(attribute, metadata) + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata diff --git a/app/views/posts/attributes/_date.haml b/app/views/posts/attributes/_date.haml index ac1c7b02..392b6b93 100644 --- a/app/views/posts/attributes/_date.haml +++ b/app/views/posts/attributes/_date.haml @@ -1,6 +1,6 @@ .form-group = label_tag "post_#{attribute}", post_label_t(attribute, post: post) - = date_field 'post', attribute, value: metadata.value.strftime('%F'), + = date_field 'post', attribute, value: metadata.value.to_date.strftime('%F'), **field_options(attribute, metadata) = render 'posts/attribute_feedback', post: post, attribute: attribute, metadata: metadata diff --git a/app/views/posts/attributes/_email.haml b/app/views/posts/attributes/_email.haml new file mode 100644 index 00000000..b2a301c3 --- /dev/null +++ b/app/views/posts/attributes/_email.haml @@ -0,0 +1,6 @@ +.form-group + = label_tag "post_#{attribute}", post_label_t(attribute, post: post) + = email_field 'post', attribute, value: metadata.value, + **field_options(attribute, metadata) + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata diff --git a/app/views/posts/attributes/_geo.haml b/app/views/posts/attributes/_geo.haml new file mode 100644 index 00000000..839393d8 --- /dev/null +++ b/app/views/posts/attributes/_geo.haml @@ -0,0 +1,19 @@ +.row + .col + .form-group + = label_tag "post_#{attribute}_lat", + post_label_t(attribute, :lat, post: post) + = text_field(*field_name_for('post', attribute, :lat), + value: metadata.value['lat'], + **field_options(attribute, metadata)) + = render 'posts/attribute_feedback', + post: post, attribute: [attribute, :lat], metadata: metadata + .col + .form-group + = label_tag "post_#{attribute}_lng", + post_label_t(attribute, :lng, post: post) + = text_field(*field_name_for('post', attribute, :lng), + value: metadata.value['lng'], + **field_options(attribute, metadata)) + = render 'posts/attribute_feedback', + post: post, attribute: [attribute, :lat], metadata: metadata diff --git a/app/views/posts/attributes/_related_posts.haml b/app/views/posts/attributes/_related_posts.haml new file mode 100644 index 00000000..8984cb4e --- /dev/null +++ b/app/views/posts/attributes/_related_posts.haml @@ -0,0 +1,19 @@ +.form-group + = label_tag "post_#{attribute}", post_label_t(attribute, post: post) + + .mapable{ data: { values: metadata.value.to_json, + 'default-values': metadata.values.to_json, + name: "post[#{attribute}][]", list: id_for_datalist(attribute), + remove: 'false', legend: post_label_t(attribute, post: post), + described: id_for_help(attribute) } } + + = text_field(*field_name_for('post', attribute, '[]'), + value: metadata.value.join(', '), + **field_options(attribute, metadata)) + + = render 'posts/attribute_feedback', + post: post, attribute: attribute, metadata: metadata + + %datalist{ id: id_for_datalist(attribute) } + - metadata.values.keys.each do |value| + %option{ value: value } diff --git a/app/views/posts/attributes/_uuid.haml b/app/views/posts/attributes/_uuid.haml new file mode 100644 index 00000000..0aab9802 --- /dev/null +++ b/app/views/posts/attributes/_uuid.haml @@ -0,0 +1 @@ +-# nada diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index 4c903485..7e0ce3a9 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -25,62 +25,61 @@ %section.col = render 'layouts/flash' - - if @posts.present? - .row - .col - = form_tag site_posts_reorder_path, method: :post do - = submit_tag t('posts.reorder'), class: 'btn submit-reorder d-none' - -# TODO: Permitir cambiar el idioma - %table.table.table-condensed.table-draggable - %tbody - - @posts.each_with_index do |post, i| - -# - saltearse el post a menos que esté en la categoría por - la que estamos filtrando - - if @category - - next unless post.attributes.include? :categories - - next unless post.categories.value.include?(@category) - - if @layout - - next unless post.layout.name == @layout - - next unless policy(post).show? - %tr - %td - .handle - = image_tag 'arrows-alt-v.svg' - = hidden_field 'post[reorder]', post.sha1, - value: i, class: 'reorder' - %td - %small - = link_to post.layout.name.to_s.humanize, - site_posts_path(@site, layout: post.layout.name) - %br/ - = link_to post.title.value, - site_post_path(@site, post.id) - - if post.attributes.include? :draft - - if post.draft.value - %span.badge.badge-primary - = post_label_t(:draft, post: post) - - if post.attributes.include? :categories - - unless post.categories.value.empty? - %br - %small - - post.categories.value.each do |c| - = link_to c, site_posts_path(@site, category: c) - - %td - = post.date.value.strftime('%F') - %br/ - = post.try(:order).try(:value) - %td - - if policy(post).edit? - = link_to t('posts.edit'), - edit_site_post_path(@site, post.id), - class: 'btn' - - if policy(post).destroy? - = link_to t('posts.destroy'), - site_post_path(@site, post.id), - class: 'btn', - method: :delete, - data: { confirm: t('posts.confirm_destroy') } - - else + - if @posts.empty? %h2= t('posts.none') + - else + = form_tag site_posts_reorder_path, method: :post do + = submit_tag t('posts.reorder'), class: 'btn submit-reorder' + -# TODO: Permitir cambiar el idioma + %table.table.table-condensed.table-draggable + %tbody + - @posts.each_with_index do |post, i| + -# + saltearse el post a menos que esté en la categoría por + la que estamos filtrando + - if @category + - next unless post.attributes.include? :categories + - next unless post.categories.value.include?(@category) + - if @layout + - next unless post.layout.name == @layout + - next unless @usuarie || policy(post).show? + %tr + %td + .handle + = image_tag 'arrows-alt-v.svg' + -# Orden más alto es mayor prioridad + = hidden_field 'post[reorder]', post.uuid.value, + value: @posts.length - i, class: 'reorder' + %td + %small + = link_to post.layout.name.to_s.humanize, + site_posts_path(@site, layout: post.layout.name) + %br/ + = link_to post.title.value, + site_post_path(@site, post.id) + - if post.attributes.include? :draft + - if post.draft.value + %span.badge.badge-primary + = post_label_t(:draft, post: post) + - if post.attributes.include? :categories + - unless post.categories.value.empty? + %br + %small + - post.categories.value.each do |c| + = link_to c, site_posts_path(@site, category: c) + + %td + = post.date.value.strftime('%F') + %br/ + = post.try(:order).try(:value) + %td + - if @usuarie || policy(post).edit? + = link_to t('posts.edit'), + edit_site_post_path(@site, post.id), + class: 'btn' + - if @usuarie || policy(post).destroy? + = link_to t('posts.destroy'), + site_post_path(@site, post.id), + class: 'btn', + method: :delete, + data: { confirm: t('posts.confirm_destroy') } diff --git a/app/views/posts/show.haml b/app/views/posts/show.haml index 86c4822b..5cac7f4b 100644 --- a/app/views/posts/show.haml +++ b/app/views/posts/show.haml @@ -26,6 +26,7 @@ = render("posts/attribute_ro/#{metadata.type}", post: @post, attribute: attr, metadata: metadata, + site: @site, tags: all_html_tags) -# Mostrar todo lo que no va en el front_matter (el contenido) @@ -33,4 +34,4 @@ - next if @post.send(attr).front_matter? %section{ id: attr } - = sanitize_markdown @post.send(attr).value, tags: all_html_tags + = raw @post.send(attr).value diff --git a/app/views/sites/_build.haml b/app/views/sites/_build.haml index d85e83cf..ca68765c 100644 --- a/app/views/sites/_build.haml +++ b/app/views/sites/_build.haml @@ -7,7 +7,7 @@ link: nil - else = form_tag site_enqueue_path(site), - method: :post, class: 'form-inline' do + method: :post, class: 'form-inline inline' do = button_tag type: 'submit', class: 'btn no-border-radius', title: t('help.sites.enqueue'), diff --git a/app/views/sites/_form.haml b/app/views/sites/_form.haml index 0afb3ada..30e72bcf 100644 --- a/app/views/sites/_form.haml +++ b/app/views/sites/_form.haml @@ -10,7 +10,7 @@ = f.text_field :name, class: form_control(site, :name), required: true, - pattern: '^([a-z0-9][a-z0-9\-]*)?[a-z0-9]$', + pattern: '^([a-z0-9][a-z0-9\-]*)?[a-z0-9\.]$', minlength: 1, maxlength: 63 - if invalid? site, :name @@ -86,14 +86,20 @@ %hr/ - .form-group - %h2= t('.deploys.title') - %p.lead= t('.help.deploys') + - if site.persisted? + .form-group + %h2= t('.deploys.title') + %p.lead= t('.help.deploys') + = f.fields_for :deploys do |deploy| + = render "deploys/#{deploy.object.type.underscore}", + deploy: deploy, site: site + %hr/ + - else = f.fields_for :deploys do |deploy| - = render "deploys/#{deploy.object.type.underscore}", - deploy: deploy, site: site - %hr/ + - next unless deploy.object.is_a? DeployLocal + + = deploy.hidden_field :type .form-group = f.submit submit, class: 'btn btn-lg btn-block' diff --git a/app/views/sites/edit.haml b/app/views/sites/edit.haml index 7edbb057..cc5977cf 100644 --- a/app/views/sites/edit.haml +++ b/app/views/sites/edit.haml @@ -1,8 +1,8 @@ = render 'layouts/breadcrumb', crumbs: [link_to(t('sites.index.title'), sites_path), t('.title', site: @site.name)] -.row - .col +.row.justify-content-center + .col-md-8 %h1= t('.title', site: @site.name) = render 'form', site: @site, submit: t('.submit') diff --git a/app/views/sites/index.haml b/app/views/sites/index.haml index 0cd3562c..759e9e38 100644 --- a/app/views/sites/index.haml +++ b/app/views/sites/index.haml @@ -9,6 +9,9 @@ class: 'btn' %section.col + - if @sites.empty? + :markdown + #{t('.welcome')} %table.table.table-condensed %tbody - @sites.each do |site| diff --git a/app/views/sites/new.haml b/app/views/sites/new.haml index f4efbea0..fa724421 100644 --- a/app/views/sites/new.haml +++ b/app/views/sites/new.haml @@ -1,8 +1,9 @@ = render 'layouts/breadcrumb', crumbs: [link_to(t('sites.index.title'), sites_path), t('.title')] -.row - .col +.row.justify-content-center + .col-md-8 %h1= t('.title') + %p.lead= t('.help') = render 'form', site: @site, submit: t('.submit') diff --git a/config/blazer.yml b/config/blazer.yml new file mode 100644 index 00000000..14501c81 --- /dev/null +++ b/config/blazer.yml @@ -0,0 +1,77 @@ +# see https://github.com/ankane/blazer for more info + +data_sources: + main: + url: <%= ENV["BLAZER_DATABASE_URL"] %> + + # statement timeout, in seconds + # none by default + # timeout: 15 + + # caching settings + # can greatly improve speed + # off by default + # cache: + # mode: slow # or all + # expires_in: 60 # min + # slow_threshold: 15 # sec, only used in slow mode + + # wrap queries in a transaction for safety + # not necessary if you use a read-only user + # true by default + # use_transaction: false + + smart_variables: + site_id: 'select id, name from sites order by name asc' + # zone_id: "SELECT id, name FROM zones ORDER BY name ASC" + # period: ["day", "week", "month"] + # status: {0: "Active", 1: "Archived"} + + linked_columns: + # user_id: "/admin/users/{value}" + + smart_columns: + site_id: 'select id, name from sites where id in {value}' + # user_id: "SELECT id, name FROM users WHERE id IN {value}" + +# create audits +audit: true + +# change the time zone +# time_zone: "Pacific Time (US & Canada)" + +# class name of the user model +user_class: Usuarie + +# method name for the current user +user_method: current_usuarie + +# method name for the display name +user_name: email + +# custom before_action to use for auth +# before_action_method: require_admin + +# email to send checks from +from_email: blazer@<%= ENV.fetch('SUTTY', 'sutty.nl') %> + +# webhook for Slack +# slack_webhook_url: <%= ENV["BLAZER_SLACK_WEBHOOK_URL"] %> + +check_schedules: + - "1 day" + - "1 hour" + - "5 minutes" + +override_csp: true + +# enable anomaly detection +# note: with trend, time series are sent to https://trendapi.org +# anomaly_checks: trend / r + +# enable forecasting +# note: with trend, time series are sent to https://trendapi.org +# forecasting: trend + +# enable map +# mapbox_access_token: <%= ENV["MAPBOX_ACCESS_TOKEN"] %> diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index cccb03cb..7040a8d1 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -6,31 +6,35 @@ # For further information see the following documentation # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy -# Rails.application.config.content_security_policy do |policy| -# policy.default_src :self, :https -# policy.font_src :self, :https, :data -# policy.img_src :self, :https, :data -# policy.object_src :none -# policy.script_src :self, :https -# policy.style_src :self, :https -# # If you are using webpack-dev-server then specify -# # webpack-dev-server host -# policy.connect_src :self, :https, "http://localhost:3035", -# "ws://localhost:3035" if Rails.env.development? +Rails.application.config.content_security_policy do |policy| + policy.default_src :self + # XXX: Varios scripts generan estilos en línea + policy.style_src :self, :unsafe_inline + # Repetimos la default para poder saber cuál es la política en falta + policy.script_src :self + policy.font_src :self + # TODO: Permitimos cargar imágenes remotas? + # XXX: Los íconos de Trix se cargan vía data: + policy.img_src :self, :data + # Ya no usamos applets! + policy.object_src :none + if Rails.env.development? + policy.connect_src :self, + 'http://localhost:3035', + 'ws://localhost:3035' + end -# # Specify URI for violation reports -# # policy.report_uri "/csp-violation-report-endpoint" -# end + # Specify URI for violation reports + policy.report_uri "https://api.#{ENV.fetch('SUTTY_WITH_PORT', 'sutty.nl')}/v1/csp_reports.json" +end # If you are using UJS then enable automatic nonce generation -# Rails.application.config.content_security_policy_nonce_generator = -# -> request { SecureRandom.base64(16) } +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } # Set the nonce only to specific directives -# Rails.application.config.content_security_policy_nonce_directives = -# %w(script-src) +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) # Report CSP violations to a specified URI # For further information see the following documentation: # https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only -# Rails.application.config.content_security_policy_report_only = true +Rails.application.config.content_security_policy_report_only = false diff --git a/config/initializers/core_extensions.rb b/config/initializers/core_extensions.rb index 95cf9571..be441390 100644 --- a/config/initializers/core_extensions.rb +++ b/config/initializers/core_extensions.rb @@ -1,6 +1,36 @@ # frozen_string_literal: true -require 'jekyll/document' - String.include CoreExtensions::String::StripTags Jekyll::Document.include CoreExtensions::Jekyll::Document::Path + +# Lazy Loading de Jekyll, deshabilitando la instanciación de elementos +# que no necesitamos +# +# TODO: Aplicar monkey patches en otro lado... +module Jekyll + Site.class_eval do + def setup + ensure_not_in_dest + end + end + + Reader.class_eval do + def retrieve_posts(_); end + + def retrieve_dirs(_, _, _); end + + def retrieve_pages(_, _); end + + def retrieve_static_files(_, _); end + end + + ThemeAssetsReader.class_eval do + def read; end + end + + # Prevenir la lectura del documento + Document.class_eval do + alias_method :read!, :read + def read; end + end +end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index e503bfc9..4980a93b 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -24,7 +24,7 @@ Devise.setup do |config| # config.mailer = 'Devise::Mailer' # Configure the parent class responsible to send e-mails. - # config.parent_mailer = 'ActionMailer::Base' + config.parent_mailer = 'ApplicationMailer' # ==> ORM configuration # Load and configure the ORM. Supports :active_record (default) and @@ -197,7 +197,7 @@ Devise.setup do |config| # website without confirming their account. # Default is 0.days, meaning the user cannot access the website # without confirming their account. - # config.allow_unconfirmed_access_for = 2.days + config.allow_unconfirmed_access_for = 2.days # A period that the user is allowed to confirm their account before # their token becomes invalid. For example, if set to 3.days, the user diff --git a/config/initializers/hosts.rb b/config/initializers/hosts.rb index 05ed2f5b..58ee2e39 100644 --- a/config/initializers/hosts.rb +++ b/config/initializers/hosts.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true Rails.application.configure do - next if ENV['RAILS_ENV'] == 'test' + next unless ENV['RAILS_ENV'] == 'development' domain = ENV.fetch('SUTTY', 'sutty.nl') diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index be6fedc5..1836ef30 100644 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,6 +1,3 @@ # frozen_string_literal: true -# Be sure to restart your server when you modify this file. - -# Add new mime types for use in respond_to blocks: -# Mime::Type.register "text/richtext", :rtf +Mime::Type.register 'application/csp-report', :json diff --git a/config/locales/devise.views.en.yml b/config/locales/devise.views.en.yml index d4c60db0..fd041b33 100644 --- a/config/locales/devise.views.en.yml +++ b/config/locales/devise.views.en.yml @@ -44,7 +44,7 @@ en: locked: Your account is locked. not_found_in_database: Invalid %{authentication_keys} or password. timeout: Your session expired. Please sign in again to continue. - unauthenticated: You need to sign in or sign up before continuing. + unauthenticated: Hi! You need to sign in or sign up before managing your sites. unconfirmed: You have to confirm your email address before continuing. mailer: confirmation_instructions: @@ -103,6 +103,7 @@ en: we_need_your_current_password_to_confirm_your_changes: We need your current password to confirm your changes new: sign_up: Sign up + help: We only ask for an e-mail address and a password. The password is safely stored, no one else besides you knows it! You'll also receive an e-mail to confirm your account. signed_up: Welcome! You have signed up successfully. signed_up_but_inactive: You have signed up successfully. However, we could not sign you in because your account is not yet activated. signed_up_but_locked: You have signed up successfully. However, we could not sign you in because your account is locked. diff --git a/config/locales/devise.views.es.yml b/config/locales/devise.views.es.yml index 508930c1..d0f57934 100644 --- a/config/locales/devise.views.es.yml +++ b/config/locales/devise.views.es.yml @@ -16,7 +16,7 @@ es: last_sign_in_ip: IP del último inicio locked_at: Fecha de bloqueo password: Contraseña - password_confirmation: Confirmación de la contraseña + password_confirmation: Confirma tu contraseña remember_created_at: Fecha de 'Recordarme' remember_me: Recordarme reset_password_sent_at: Fecha de envío de código para contraseña @@ -44,7 +44,7 @@ es: locked: Tu cuenta está bloqueada. not_found_in_database: "%{authentication_keys} o contraseña inválidos." timeout: Tu sesión expiró. Por favor, inicia sesión nuevamente para continuar. - unauthenticated: Tienes que iniciar sesión o registrarte para poder continuar. + unauthenticated: "¡Hola! Tienes que iniciar sesión o registrarte para poder gestionar tus sitios." unconfirmed: Te enviamos un correo electrónico para confirmar tu cuenta, por favor acéptalo para poder continuar. mailer: confirmation_instructions: @@ -80,7 +80,7 @@ es: edit: change_my_password: Cambiar mi contraseña change_your_password: Cambia tu contraseña - confirm_new_password: Confirme la nueva contraseña + confirm_new_password: Confirma la nueva contraseña new_password: Nueva contraseña new: forgot_your_password: "¿Has olvidado tu contraseña?" @@ -103,7 +103,7 @@ es: we_need_your_current_password_to_confirm_your_changes: Necesitamos tu contraseña actual para confirmar los cambios. new: sign_up: Registrarme por primera vez - email: O simplemente continuar con tu dirección de correo y contraseña + help: Para registrarte solo pedimos una dirección de correo y una contraseña. La contraseña se almacena de forma segura, ¡nadie más que vos la sabe! Recibirás un correo de confirmación de cuenta. signed_up: Bienvenide. Tu cuenta fue creada. signed_up_but_inactive: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque tu cuenta aún no está activada. signed_up_but_locked: Tu cuenta ha sido creada correctamente. Sin embargo, no hemos podido iniciar la sesión porque que tu cuenta está bloqueada. diff --git a/config/locales/en.yml b/config/locales/en.yml index 84215d03..18b7a441 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -148,7 +148,6 @@ en: anexo: 'Appendix' simple: 'Simple' sites: - static_file_migration: 'File migration' index: 'This is the list of sites you can edit.' edit_translations: "You can edit texts from your site other than posts', and you can also translate them to other languages." @@ -247,11 +246,27 @@ en: average: 'Average building time' maximum: 'Maximum building time' sites: + static_file_migration: 'File migration' index: title: 'Sites' pull: 'Upgrade' help: 'This is the list of sites you can edit' - visit: 'Visitar el sitio' + visit: 'Visit the site' + welcome: | + # Welcome! + + You have no sites yet. You can generate all the sites you want + with the **Create site** button. + + Para ver los cambios, usa el botón **Publicar cambios** en cada + sitio y espera unos segundos. También recibirás un correo de + notificación. + + To see your changes, use the **Publish changes** button on each + site and wait a few seconds. You'll also receive an e-mail + notification. + + [Create my first site](/sites/new) repository: config: 'Changes in config' actions: 'Actions' @@ -332,6 +347,9 @@ en: en: 'English' ar: 'Arabic' posts: + attribute_ro: + file: + download: Download file show: front_matter: Post metadata submit: @@ -348,6 +366,11 @@ en: required: label: ' (required)' feedback: 'This field cannot be empty!' + uuid: + label: 'Unique identifier' + geo: + uri: 'Open in app' + osm: 'Open in web map' reorder: 'Reorder posts' sort: by: 'Sort by' diff --git a/config/locales/es.yml b/config/locales/es.yml index 2c71813e..f988972f 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -71,7 +71,7 @@ es: usuarie: email: 'Correo electrónico' password: 'Contraseña' - password_confirmation: 'Confirmación de contraseña' + password_confirmation: 'Confirma tu contraseña' current_password: 'Contraseña actual' lang: Idioma principal remember_me: Recordarme @@ -149,7 +149,6 @@ es: anexo: 'Anexo' simple: 'Simple' sites: - static_file_migration: 'Migración de archivos' index: 'Este es el listado de sitios que puedes editar.' edit_translations: 'Puedes editar los textos que salen en tu sitio que no corresponden a artículos aquí, además de traducirlos a @@ -252,11 +251,24 @@ es: average: 'Tiempo promedio de generación' maximum: 'Tiempo máximo de generación' sites: + static_file_migration: 'Migración de archivos' index: title: 'Sitios' pull: 'Actualizar' help: 'Este es el listado de sitios que puedes editar' visit: 'Visitar el sitio' + welcome: | + # ¡Bienvenide! + + Todavía no tienes ningún sitio. Puedes crear todos los sitios + que quieras usando el botón **Crear sitio**. + + Para ver los cambios, usa el botón **Publicar cambios** en cada + sitio y espera unos segundos. También recibirás un correo de + notificación. + + [Crear mi primer sitio](/sites/new) + repository: config: 'Cambios en la configuración' actions: 'Acciones' @@ -272,6 +284,7 @@ es: new: title: 'Crear un sitio' submit: 'Crear sitio' + help: 'Podrás editar estas opciones más adelante en la configuración del sitio.' edit: title: 'Editar %{site}' submit: 'Guardar cambios' @@ -342,6 +355,9 @@ es: en: 'inglés' ar: 'árabe' posts: + attribute_ro: + file: + download: Descargar archivo show: front_matter: Metadatos del artículo submit: @@ -358,6 +374,11 @@ es: required: label: ' (requerido)' feedback: '¡Este campo no puede estar vacío!' + uuid: + label: 'Identificador único' + geo: + uri: 'Abrir en aplicación' + osm: 'Abrir en mapa web' reorder: 'Reordenar artículos' sort: by: 'Ordenar por' diff --git a/config/puma.rb b/config/puma.rb index 872346ff..629b81fe 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -38,3 +38,22 @@ preload_app! plugin :tmp_restart pidfile 'tmp/puma.pid' + +# If you are preloading your application and using Active Record, it's +# recommended that you close any connections to the database before workers +# are forked to prevent connection leakage. +# +before_fork do + ActiveRecord::Base.connection_pool.disconnect! if defined?(ActiveRecord) +end + +# The code in the `on_worker_boot` will be called if you are using +# clustered mode by specifying a number of `workers`. After each worker +# process is booted, this block will be run. If you are using the `preload_app!` +# option, you will want to use this block to reconnect to any threads +# or connections that may have been created at application boot, as Ruby +# cannot share connections between processes. +# +on_worker_boot do + ActiveRecord::Base.establish_connection if defined?(ActiveRecord) +end diff --git a/config/routes.rb b/config/routes.rb index 8ef82cbc..d8b5f391 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,6 +2,7 @@ Rails.application.routes.draw do devise_for :usuaries + mount Blazer::Engine, at: 'blazer' root 'application#index' @@ -18,6 +19,7 @@ Rails.application.routes.draw do constraints subdomain: 'api' do scope module: 'api' do namespace :v1 do + resources :csp_reports, only: %i[create] get 'sites/allowed', to: 'sites#allowed' resources :sites, only: %i[index] end diff --git a/config/webpacker.yml b/config/webpacker.yml index 46ed57dd..c37bb708 100644 --- a/config/webpacker.yml +++ b/config/webpacker.yml @@ -57,10 +57,10 @@ development: # Reference: https://webpack.js.org/configuration/dev-server/ dev_server: - https: false - host: localhost + https: true + host: <%= ENV.fetch('SUTTY', 'localhost') %> port: 3035 - public: localhost:3035 + public: <%= ENV.fetch('SUTTY', 'localhost') %>:3035 hmr: false # Inline should be set to true if using HMR inline: true diff --git a/db/migrate/20200118155319_create_access_log.rb b/db/migrate/20200118155319_create_access_log.rb new file mode 100644 index 00000000..8b4eaf46 --- /dev/null +++ b/db/migrate/20200118155319_create_access_log.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +class CreateAccessLog < ActiveRecord::Migration[6.0] + def change + create_table :access_logs, id: :uuid do |t| + t.string :host, index: true + + t.float :msec + + t.string :server_protocol + t.string :request_method + t.string :request_completion + + t.string :uri, index: true + t.string :query_string + t.integer :status, index: true + + t.string :sent_http_content_type + t.string :sent_http_content_encoding + t.string :sent_http_etag + t.datetime :sent_http_last_modified + + t.string :http_accept + t.string :http_accept_encoding + t.string :http_accept_language + + t.string :http_pragma + t.string :http_cache_control + t.string :http_if_none_match + + t.string :http_dnt + t.string :http_user_agent, index: true + t.string :http_referer, index: true + + t.float :request_time + + t.integer :bytes_sent + t.integer :body_bytes_sent + t.integer :request_length + + t.string :http_connection + t.string :pipe + t.integer :connection_requests + + t.string :geoip2_data_country_name, index: true + t.string :geoip2_data_city_name, index: true + + t.string :ssl_server_name + t.string :ssl_protocol + t.string :ssl_early_data + t.string :ssl_session_reused + t.string :ssl_curves + t.string :ssl_ciphers + t.string :ssl_cipher + + t.string :sent_http_x_xss_protection + t.string :sent_http_x_frame_options + t.string :sent_http_x_content_type_options + t.string :sent_http_strict_transport_security + + t.string :nginx_version + t.integer :pid + end + end +end diff --git a/db/migrate/20200126175158_change_last_modified.rb b/db/migrate/20200126175158_change_last_modified.rb new file mode 100644 index 00000000..93d05dec --- /dev/null +++ b/db/migrate/20200126175158_change_last_modified.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class ChangeLastModified < ActiveRecord::Migration[6.0] + def change + change_column :access_logs, :sent_http_last_modified, :string + end +end diff --git a/db/migrate/20200205173039_create_csp_reports.rb b/db/migrate/20200205173039_create_csp_reports.rb new file mode 100644 index 00000000..2a0f4051 --- /dev/null +++ b/db/migrate/20200205173039_create_csp_reports.rb @@ -0,0 +1,17 @@ +class CreateCspReports < ActiveRecord::Migration[6.0] + def change + create_table :csp_reports, id: :uuid do |t| + t.timestamps + + t.string :disposition + t.string :referrer + t.string :blocked_uri + t.string :document_uri + t.string :effective_directive + t.string :original_policy + t.string :script_sample + t.string :status_code + t.string :violated_directive + end + end +end diff --git a/db/migrate/20200206151057_add_source_to_csp_reports.rb b/db/migrate/20200206151057_add_source_to_csp_reports.rb new file mode 100644 index 00000000..9b1dd8fb --- /dev/null +++ b/db/migrate/20200206151057_add_source_to_csp_reports.rb @@ -0,0 +1,7 @@ +class AddSourceToCspReports < ActiveRecord::Migration[6.0] + def change + add_column :csp_reports, :column_number, :integer + add_column :csp_reports, :line_number, :integer + add_column :csp_reports, :source_file, :string + end +end diff --git a/db/migrate/20200206163257_install_blazer.rb b/db/migrate/20200206163257_install_blazer.rb new file mode 100644 index 00000000..9b084169 --- /dev/null +++ b/db/migrate/20200206163257_install_blazer.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# Blazer +class InstallBlazer < ActiveRecord::Migration[6.0] + def change + return unless Rails.env.production? + + create_table :blazer_queries do |t| + t.references :creator + t.string :name + t.text :description + t.text :statement + t.string :data_source + t.timestamps null: false + end + + create_table :blazer_audits do |t| + t.references :user + t.references :query + t.text :statement + t.string :data_source + t.timestamp :created_at + end + + create_table :blazer_dashboards do |t| + t.references :creator + t.text :name + t.timestamps null: false + end + + create_table :blazer_dashboard_queries do |t| + t.references :dashboard + t.references :query + t.integer :position + t.timestamps null: false + end + + create_table :blazer_checks do |t| + t.references :creator + t.references :query + t.string :state + t.string :schedule + t.text :emails + t.text :slack_channels + t.string :check_type + t.text :message + t.timestamp :last_run_at + t.timestamps null: false + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 6d63cb91..5d0d00a8 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,7 +12,10 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_190_829_180_743) do +ActiveRecord::Schema.define(version: 20_200_206_151_057) do + # Could not dump table "access_logs" because of following StandardError + # Unknown type '' for column 'id' + create_table 'action_text_rich_texts', force: :cascade do |t| t.string 'name', null: false t.text 'body' @@ -55,6 +58,9 @@ ActiveRecord::Schema.define(version: 20_190_829_180_743) do t.index ['deploy_id'], name: 'index_build_stats_on_deploy_id' end + # Could not dump table "csp_reports" because of following StandardError + # Unknown type 'uuid' for column 'id' + create_table 'deploys', force: :cascade do |t| t.datetime 'created_at', null: false t.datetime 'updated_at', null: false @@ -132,6 +138,7 @@ ActiveRecord::Schema.define(version: 20_190_829_180_743) do t.string 'status', default: 'waiting' t.text 'description' t.string 'title' + t.boolean 'colaboracion_anonima', default: false t.index ['design_id'], name: 'index_sites_on_design_id' t.index ['licencia_id'], name: 'index_sites_on_licencia_id' t.index ['name'], name: 'index_sites_on_name', unique: true diff --git a/db/seeds/designs.yml b/db/seeds/designs.yml index 873ceb4c..328265fe 100644 --- a/db/seeds/designs.yml +++ b/db/seeds/designs.yml @@ -4,15 +4,15 @@ gem: 'sutty-theme-none' url: 'https://sutty.nl' disabled: true - description_en: "Upload your own theme. [This feature is in development, help us!](https://sutty.neocities.org/en/#contact)" - description_es: "Subir tu propio diseño. [Esta posibilidad está en desarrollo, ¡ayudanos!](https://sutty.neocities.org/es/#contacto)" + description_en: "Upload your own theme. [This feature is in development, help us!](https://sutty.nl/en/#contact)" + description_es: "Subir tu propio diseño. [Esta posibilidad está en desarrollo, ¡ayudanos!](https://sutty.nl/#contacto)" - name_en: 'I want you to create a site for me' name_es: 'Quiero que desarrollen mi sitio' gem: 'sutty-theme-custom' url: 'https://sutty.nl' disabled: true - description_en: "If you want us to create your site, you're welcome to [contact us!](https://sutty.neocities.org/en/#contact) :)" - description_es: "Si querés que desarrollemos tu sitio, [escribinos](https://sutty.neocities.org/es/#contacto) :)" + description_en: "If you want us to create your site, you're welcome to [contact us!](https://sutty.nl/en/#contact) :)" + description_es: "Si querés que desarrollemos tu sitio, [escribinos](https://sutty.nl/#contacto) :)" - name_en: 'Minima' name_es: 'Mínima' gem: 'minima' @@ -25,5 +25,5 @@ gem: 'sutty-theme-own' url: 'https://jekyllthemes.org' disabled: true - description_en: "We're working to add more themes for you to use. [Contact us!](https://sutty.neocities.org/en/#contact)" - description_es: "Estamos trabajando para que puedas tener más diseños. [¡Escribinos!](https://sutty.neocities.org/es/#contacto)" + description_en: "We're working to add more themes for you to use. [Contact us!](https://sutty.nl/en/#contact)" + description_es: "Estamos trabajando para que puedas tener más diseños. [¡Escribinos!](https://sutty.nl/#contacto)" diff --git a/db/seeds/sites.yml b/db/seeds/sites.yml index 1485f0f9..9cc14ce1 100644 --- a/db/seeds/sites.yml +++ b/db/seeds/sites.yml @@ -17,3 +17,4 @@ - name: mail - name: email - name: xmpp +- name: radicale diff --git a/doc/reordenar.md b/doc/reordenar.md index 99a9ed07..e40e92c8 100644 --- a/doc/reordenar.md +++ b/doc/reordenar.md @@ -23,3 +23,11 @@ otra vez. Lo más controlado sería enviar exactamente el id del post con su nueva ubicación en el orden. Esta es la implementación anterior. + +*** + +El orden es descendiente (fechas más nuevas primero), pero el orden que +estuvimos usando es ascendientes (números más bajos primero). Es más +simple invertir la lógica y hacer todo el orden descendiente. Para eso +los artículos más nuevos tienen que tener el número de orden +correspondiente a la posición en el array ordenado por fecha. diff --git a/doc/uuid.md b/doc/uuid.md new file mode 100644 index 00000000..00c6ffd8 --- /dev/null +++ b/doc/uuid.md @@ -0,0 +1,28 @@ +# Identificadores para los artículos + +Para poder vincular artículos entre sí y para otros usos, necesitamos +identificarlos únicamente. Un identificador incremental es problemático +porque tendríamos que mantener el estado y poder responder preguntas +como ¿cuál es el último identificador que asignamos? + +Para poder identificar artículos sin mantener estado, usamos +[UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier), +que son cadenas aleatorias que se pueden asignar adhoc. Así, en lugar +de un ID numérico que va incrementando, podemos asociar cadenas al +estilo `fb4a5048-5fa1-4b85-b70e-6c502feecdb9` (generada con la +herramienta `uuidgen`). + +## MetadataUUID + +Cada artículo se crea con un metadato `uuid` cuyo valor por defecto es +un UUID autogenerado utilizando `SecureRandom.uuid`. Este valor no +cambia (a menos que se lo vacíe intencionalmente). + +## Migración + +Para todos los artículos que existen, hay que escribir una migración que +se los agregue. + +Para esto hay que cargar sitio por sitio, recorrer los artículos +asignando un UUID y guardando todos los cambios como un solo commit de +git. diff --git a/monit.conf b/monit.conf index 4d34d69c..b612016b 100644 --- a/monit.conf +++ b/monit.conf @@ -1,5 +1,6 @@ check process sutty with pidfile /srv/http/tmp/puma.pid - start program = "/bin/sh -c 'cd /srv/http && foreman start migrate && foreman start sutty'" as uid app + start program = "/bin/sh -c 'cd /srv/http && foreman start migrate && foreman start sutty'" + as uid "app" and gid "www-data" stop program = "/bin/sh -c 'cat /srv/http/tmp/puma.pid | xargs kill'" check program sync_assets diff --git a/package.json b/package.json index 1f9e08ad..3106dad1 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "@rails/activestorage": "^6.0.0", "@rails/webpacker": "^4.0.7", "commonmark": "^0.29.0", + "input-map": "https://0xacab.org/sutty/input-map.git", "input-tag": "https://0xacab.org/sutty/input-tag.git", "prosemirror-commands": "^1.0.8", "prosemirror-gapcursor": "^1.0.4", diff --git a/sync_assets.sh b/sync_assets.sh index 167d6205..1c1a6ca1 100644 --- a/sync_assets.sh +++ b/sync_assets.sh @@ -2,4 +2,4 @@ # Sincronizar assets desde public a _public para que estén disponibles # en el contenedor web. -rsync -a --delete-after public/ _public/ +rsync -a --delete-after /srv/http/public/ /srv/http/_public/ diff --git a/test/controllers/api/v1/csp_reports_controller_test.rb b/test/controllers/api/v1/csp_reports_controller_test.rb new file mode 100644 index 00000000..c98fd860 --- /dev/null +++ b/test/controllers/api/v1/csp_reports_controller_test.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'test_helper' + +module Api + module V1 + class CSPReportsControllerTest < ActionDispatch::IntegrationTest + test 'se puede enviar un reporte' do + post v1_csp_reports_url, + params: { + 'csp-report': { + 'document-uri': 'http://example.com/signup.html', + 'referrer': '', + 'blocked-uri': 'http://example.com/css/style.css', + 'violated-directive': 'style-src cdn.example.com', + 'original-policy': "default-src 'none'; style-src cdn.example.com; report-uri /_/csp-reports" + } + } + + assert_equal 201, response.status + assert_equal 1, CspReport.all.count + end + end + end +end diff --git a/test/controllers/posts_controller_test.rb b/test/controllers/posts_controller_test.rb index 3075948b..6a25fe30 100644 --- a/test/controllers/posts_controller_test.rb +++ b/test/controllers/posts_controller_test.rb @@ -145,7 +145,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest test 'se pueden reordenar' do lang = I18n.available_locales.sample posts = @site.posts(lang: lang) - reorder = Hash[posts.map(&:sha1).shuffle.each_with_index.to_a] + reorder = Hash[posts.map { |p| p.uuid.value }.shuffle.each_with_index.to_a] post site_posts_reorder_url(@site), headers: @authorization, @@ -157,7 +157,7 @@ class PostsControllerTest < ActionDispatch::IntegrationTest @site.repository.rugged.head.target.message assert_equal reorder, Hash[@site.posts(lang: lang).map do |p| - [p.sha1, p.order.value] + [p.uuid.value, p.order.value] end] end end diff --git a/test/models/post_test.rb b/test/models/post_test.rb index 827aeb5c..82a06cb4 100644 --- a/test/models/post_test.rb +++ b/test/models/post_test.rb @@ -66,7 +66,7 @@ class PostTest < ActiveSupport::TestCase test 'se pueden guardar sin validar' do assert @post.valid? @post.title.value = '' - assert @post.save(false) + assert @post.save(validate: false) end test 'se pueden guardar los cambios' do @@ -82,7 +82,7 @@ class PostTest < ActiveSupport::TestCase document = Jekyll::Document.new(@post.path.value, site: @site.jekyll, collection: collection) - document.read + document.read! assert document.data['categories'].include?(title) assert_equal title, document.data['title'] diff --git a/test/models/site_test.rb b/test/models/site_test.rb index 78e3f577..bb6a4777 100644 --- a/test/models/site_test.rb +++ b/test/models/site_test.rb @@ -32,11 +32,11 @@ class SiteTest < ActiveSupport::TestCase assert_not site.errors.messages[:name].present? end - test 'el nombre del sitio no puede terminar con punto' do + test 'el nombre del sitio puede terminar con punto' do site = build :site, name: 'hola.chau.' site.validate - assert site.errors.messages[:name].present? + assert_not site.errors.messages[:name].present? end test 'el nombre del sitio no puede contener wildcard' do diff --git a/vendor/.keep b/vendor/.keep deleted file mode 100644 index e69de29b..00000000 diff --git a/yarn.lock b/yarn.lock index 6aea8946..b0967187 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3401,6 +3401,10 @@ ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" integrity sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw== +"input-map@https://0xacab.org/sutty/input-map.git": + version "0.0.7" + resolved "https://0xacab.org/sutty/input-map.git#e2a356c63e4e956f74dc26527bacb2c7c82c8773" + "input-tag@https://0xacab.org/sutty/input-tag.git": version "0.0.6" resolved "https://0xacab.org/sutty/input-tag.git#5bfcbcb83abd941caadab09f80cc684909f12a37"