5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-17 09:36:23 +00:00

Merge branch 'rails' into prosemirror

This commit is contained in:
void 2020-02-28 12:24:26 -03:00
commit 2d492538db
107 changed files with 1166 additions and 498 deletions

View file

@ -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=

3
.gitignore vendored
View file

@ -39,3 +39,6 @@
yarn-debug.log*
.yarn-integrity
/vendor
*.key
*.crt

View file

@ -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'

View file

@ -1 +1 @@
2.5.7
2.6.5

View file

@ -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 <f@sutty.nl>"
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"

21
Gemfile
View file

@ -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

View file

@ -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

View file

@ -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

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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 .

View file

@ -7,4 +7,13 @@ $(document).on('turbolinks:load', function() {
props: { ...this.dataset }
});
});
$('.mapable').each(function() {
this.innerHTML = '';
new InputMap({
target: this,
props: { ...this.dataset }
});
});
});

View file

@ -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);

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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'

View file

@ -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

View file

@ -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'))
})
})

View file

@ -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

4
app/models/csp_report.rb Normal file
View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
# Almacena un reporte de CSP
class CspReport < ApplicationRecord; end

View file

@ -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

View file

@ -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?

View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
# Un campo de correo
class MetadataColor < MetadataString; end

View file

@ -0,0 +1,5 @@
# frozen_string_literal: true
# Un campo de correo
# TODO: Validar que tenga un formato correo
class MetadataEmail < MetadataString; end

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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|

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,5 @@
= t('.greeting', recipient: @email)
\
= t('.instruction')
\
= confirmation_url(@resource, confirmation_token: @token)

View file

@ -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)

View file

@ -0,0 +1,3 @@
= t('.greeting', recipient: @resource.email)
\
= t('.message')

View file

@ -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')

View file

@ -0,0 +1,7 @@
= t('.greeting', recipient: @resource.email)
\
= t('.message')
\
= t('.instruction')
\
= unlock_url(@resource, unlock_token: @token)

View file

@ -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,

View file

@ -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?

View file

@ -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'

View file

@ -1,3 +1,4 @@
= yield
\
= '-- '
= t('.signature')

View file

@ -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)

View file

@ -0,0 +1,3 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td{ style: "background-color: #{metadata.value}" } = metadata.value

View file

@ -1,3 +1,3 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td= sanitize_markdown metadata.value, tags: tags
%td= metadata.value

View file

@ -0,0 +1,4 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td
%a{ href: "mailto:#{metadata.value}" }= metadata.value

View file

@ -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']

View file

@ -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}"

View file

@ -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)

View file

@ -0,0 +1,3 @@
%tr{ id: attribute }
%th= post_label_t(attribute, post: post)
%td= metadata.value

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }

View file

@ -0,0 +1 @@
-# nada

View file

@ -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') }

View file

@ -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

View file

@ -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'),

View file

@ -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'

View file

@ -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')

View file

@ -9,6 +9,9 @@
class: 'btn'
%section.col
- if @sites.empty?
:markdown
#{t('.welcome')}
%table.table.table-condensed
%tbody
- @sites.each do |site|

View file

@ -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')

77
config/blazer.yml Normal file
View file

@ -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"] %>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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')

View file

@ -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

View file

@ -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.

View file

@ -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.

View file

@ -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'

View file

@ -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'

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)"

View file

@ -17,3 +17,4 @@
- name: mail
- name: email
- name: xmpp
- name: radicale

View file

@ -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.

28
doc/uuid.md Normal file
View file

@ -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.

View file

@ -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

View file

@ -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",

Some files were not shown because too many files have changed in this diff Show more