5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-22 16:56:21 +00:00

Merge branch 'rails' into issue-13077

This commit is contained in:
f 2024-03-26 12:42:46 -03:00
commit 06bde9f971
No known key found for this signature in database
159 changed files with 6391 additions and 3066 deletions

View file

@ -1,8 +1,8 @@
NODE_OPTIONS=--openssl-legacy-provider
# pwgen -1 32
RAILS_MASTER_KEY=11111111111111111111111111111111
RAILS_GROUPS=assets
DELEGATE=athshe.sutty.nl
HAINISH=../haini.sh/haini.sh
DELEGATE=panel.sutty.nl
DATABASE_URL=postgres://suttier@postgresql.sutty.local/sutty
RAILS_ENV=development
IMAP_SERVER=
@ -37,3 +37,5 @@ AIRBRAKE_API_KEY=
GITLAB_URI=https://0xacab.org
GITLAB_PROJECT=
GITLAB_TOKEN=
PGVER=15
PGPID=/run/postgresql.pid

1
.env.development Normal file
View file

@ -0,0 +1 @@
HAINISH=../haini.sh/haini.sh

6
.gitignore vendored
View file

@ -28,7 +28,7 @@
/data/*
/_storage/*
.env*
.env.*
# Ignore master key for decrypting credentials and more.
/config/master.key
@ -48,3 +48,7 @@ yarn-debug.log*
/yarn-error.log
yarn-debug.log*
.yarn-integrity
/.task
/.yardoc
/public/doc/

View file

@ -1,33 +1,104 @@
image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8-panel.sutty.nl"
.apk-add: &apk-add
- "apk add go-task diffutils gitlab_ci_log_section"
.disable-hainish: &disable-hainish
- "rm -f .env.development"
.cache-ruby: &cache-ruby
- paths:
- "vendor/ruby"
- ".bundle"
.cache-node: &cache-node
- paths:
- "node_modules"
.cache-task: &cache-task
- paths:
- ".task"
image: "registry.0xacab.org/sutty/sutty:3.17.3-3.1.4-rails"
variables:
RAILS_ENV: "production"
LC_ALL: "C.UTF-8"
HAINISH: ""
cache:
paths:
- "vendor/ruby"
assets:
stage: "build"
rules:
- if: "$CI_COMMIT_BRANCH == \"panel.sutty.nl\""
- if: "$CI_COMMIT_BRANCH"
changes:
compare_to: "refs/heads/rails"
paths:
- "package.json"
- "app/javascript/**/*"
- "app/assets/**/*"
stage: "deploy"
only:
- "rails"
- "17.3.alpine.panel.sutty.nl"
except:
- "schedules"
cache:
- *cache-ruby
- *cache-node
- *cache-task
before_script:
- "gitlab_ci_log_section --name git --header=\"Configuring git\""
- "git config --global user.email \"${GIT_USER_EMAIL:-$GITLAB_USER_EMAIL}\""
- "git config --global user.name \"${GIT_USER_NAME:-$GITLAB_USER_NAME}\""
- "git remote set-url --push origin \"https://${GITLAB_USERNAME}:${GITLAB_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git\""
- "apk add python2 dotenv brotli"
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
- "cp .env.example .env"
- "dotenv bundle install --path=vendor"
- "gitlab_ci_log_section --name git --end"
- "gitlab_ci_log_section --name apk --header=\"Installing dependencies\""
- "apk add brotli"
- *apk-add
- *disable-hainish
- "gitlab_ci_log_section --name apk --end"
script:
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
- "gitlab_ci_log_section --name assets --header=\"Building\""
- "go-task assets"
after_script:
- "git add public && git commit -m \"ci: assets [skip ci]\""
- "git push -o ci.skip"
gem-audit:
stage: "test"
only:
- "schedules"
cache:
- *cache-ruby
before_script:
- *apk-add
- *disable-hainish
script:
- "go-task gem-audit"
node-audit:
stage: "test"
only:
- "schedules"
cache:
- *cache-node
before_script:
- *apk-add
- *disable-hainish
script:
- "apk add go-task"
- "go-task node-audit"
brakeman:
stage: "test"
cache:
- *cache-ruby
rules:
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
before_script:
- *apk-add
- *disable-hainish
script:
- "go-task bundle -- exec brakeman"
rubocop:
stage: "test"
cache:
- *cache-ruby
rules:
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
before_script:
- *apk-add
- *disable-hainish
script:
- "./bin/modified_files | ./bin/with_extension rb | xargs -r go-task bundle -- exec rubocop"
haml:
stage: "test"
cache:
- *cache-ruby
rules:
- if: "$CI_PIPELINE_SOURCE == 'merge_request_event'"
before_script:
- *apk-add
- *disable-hainish
script:
- "./bin/modified_files | ./bin/with_extension haml | xargs -r go-task bundle -- exec haml-lint"

View file

@ -19,7 +19,6 @@ pipeline:
when:
branch:
- "rails"
- "panel.sutty.nl"
- "17.3.alpine.panel.sutty.nl"
event: "push"
path:
@ -27,57 +26,8 @@ pipeline:
- "Dockerfile"
- ".dockerignore"
- ".woodpecker.yml"
assets:
image: "gitea.nulo.in/sutty/panel:3.14.10-2.7.8"
commands:
- "apk add python2 dotenv openssh-client brotli"
- "install -d -m 700 ~/.ssh/"
- "echo \"$${KNOWN_HOSTS}\" | base64 -d >> ~/.ssh/known_hosts"
- "chmod 600 ~/.ssh/known_hosts"
- "eval $(ssh-agent -s)"
- "echo \"$${SSH_KEY}\" | base64 -d | ssh-add -"
- "ssh $${ORIGIN%:*}"
- "git config user.name Woodpecker"
- "git config user.email ci@sutty.coop.ar"
- "git remote add upstream $${ORIGIN}"
- "git checkout -B ${CI_COMMIT_BRANCH}"
- "mv config/credentials.yml.enc.ci config/credentials.yml.enc"
- "yarn"
- "cp .env.example .env"
- "dotenv bundle install --path=vendor"
- "dotenv RAILS_ENV=production bundle exec rails webpacker:clobber"
- "dotenv RAILS_ENV=production bundle exec rails assets:precompile"
- "dotenv RAILS_ENV=production bundle exec rails assets:clean"
- "find public -type f -print0 | xargs -r0 brotli -k9f"
- "git add public && git commit -m \"ci: assets [skip ci]\""
- "git pull upstream ${CI_COMMIT_BRANCH}"
- "git push upstream ${CI_COMMIT_BRANCH}"
environment:
- "RUBY_VERSION=${RUBY_VERSION}"
- "GEMS_SOURCE=https://14.3.alpine.gems.sutty.nl"
secrets:
- "SSH_KEY"
- "KNOWN_HOSTS"
- "ORIGIN"
when:
branch:
- "rails"
- "panel.sutty.nl"
path:
include:
- "app/assets/**/*"
- "app/javascript/**/*"
- "package.json"
- "yarn.lock"
matrix:
ALPINE_VERSION: "3.14.10"
RUBY_VERSION: "2.7"
RUBY_PATCH: "8"
matrix:
include:
- ALPINE_VERSION: "3.17.3"
RUBY_VERSION: "3.1"
RUBY_PATCH: "4"
- ALPINE_VERSION: "3.14.10"
RUBY_VERSION: "2.7"
RUBY_PATCH: "8"

View file

@ -1,6 +1,6 @@
ARG RUBY_VERSION=2.7
ARG RUBY_PATCH=6
ARG ALPINE_VERSION=3.13.10
ARG RUBY_VERSION=3.1
ARG RUBY_PATCH=4
ARG ALPINE_VERSION=3.17.3
ARG BASE_IMAGE=registry.nulo.in/sutty/rails
FROM ${BASE_IMAGE}:${ALPINE_VERSION}-${RUBY_VERSION}.${RUBY_PATCH}
ARG PANDOC_VERSION=2.18

View file

@ -4,8 +4,6 @@ source ENV.fetch('GEMS_SOURCE', 'https://17.3.alpine.gems.sutty.nl')
ruby "~> #{ENV.fetch('RUBY_VERSION', '3.1')}"
gem 'dotenv-rails', require: 'dotenv/rails-now'
# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 6.1.0'
# Use Puma as the app server
@ -40,7 +38,6 @@ gem 'devise'
gem 'devise-i18n'
gem 'devise_invitable'
gem 'distributed-press-api-client', '~> 0.3.0rc0'
gem 'njalla-api-client', '~> 0.2.0'
gem 'email_address', git: 'https://github.com/fauno/email_address', branch: 'i18n'
gem 'exception_notification'
gem 'fast_blank'
@ -53,7 +50,6 @@ gem 'inline_svg'
gem 'httparty'
gem 'safe_yaml', require: false
gem 'jekyll', '~> 4.2.0'
gem 'jekyll-data'
gem 'jekyll-commonmark', '~> 1.4.0'
gem 'jekyll-images'
gem 'jekyll-include-cache'
@ -110,6 +106,7 @@ end
group :development, :test do
gem 'derailed_benchmarks'
gem 'dotenv-rails'
gem 'pry'
# Adds support for Capybara system testing and selenium driver
gem 'capybara', '~> 2.13'
@ -118,7 +115,9 @@ group :development, :test do
end
group :development do
gem 'yard'
gem 'brakeman'
gem 'bundler-audit'
gem 'haml-lint', require: false
gem 'letter_opener'
gem 'listen'

View file

@ -106,6 +106,9 @@ GEM
sassc-rails (>= 2.0.0)
brakeman (5.4.1)
builder (3.2.4)
bundler-audit (0.9.1)
bundler (>= 1.2.0, < 3)
thor (~> 1.0)
capybara (2.18.0)
addressable
mini_mime (>= 0.1.3)
@ -287,8 +290,6 @@ GEM
terminal-table (~> 2.0)
jekyll-commonmark (1.4.0)
commonmarker (~> 0.22)
jekyll-data (1.1.2)
jekyll (>= 3.3, < 5.0.0)
jekyll-images (0.4.1)
jekyll (~> 4)
ruby-filemagic (~> 0.7)
@ -366,9 +367,6 @@ GEM
net-ssh (7.1.0)
netaddr (2.0.6)
nio4r (2.5.9-x86_64-linux-musl)
njalla-api-client (0.2.0)
dry-schema
httparty (~> 0.18)
nokogiri (1.15.4-x86_64-linux-musl)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
@ -541,7 +539,7 @@ GEM
temple (0.10.1)
terminal-table (2.0.0)
unicode-display_width (~> 1.1, >= 1.1.1)
thor (1.2.2)
thor (1.3.0)
tilt (2.1.0)
timecop (0.9.6)
timeout (0.3.2)
@ -579,6 +577,7 @@ GEM
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.34)
zeitwerk (2.6.8)
PLATFORMS
@ -590,6 +589,7 @@ DEPENDENCIES
blazer
bootstrap (~> 4)
brakeman
bundler-audit
capybara (~> 2.13)
chartkick
commonmarker
@ -623,7 +623,6 @@ DEPENDENCIES
jbuilder (~> 2.5)
jekyll (~> 4.2.0)
jekyll-commonmark (~> 1.4.0)
jekyll-data
jekyll-images
jekyll-include-cache
kaminari
@ -636,7 +635,6 @@ DEPENDENCIES
mini_magick
mobility
net-ssh
njalla-api-client (~> 0.2.0)
nokogiri
pg
pg_search
@ -676,6 +674,7 @@ DEPENDENCIES
web-console
webpacker
yaml_db!
yard
RUBY VERSION
ruby 3.1.4p223

140
Makefile
View file

@ -1,140 +0,0 @@
SHELL := /bin/bash
.DEFAULT_GOAL := help
# Copiar el archivo de configuración y avisar cuando hay que
# actualizarlo.
.env: .env.example
@test -f $@ || cp -v $< $@
@test -f $@ && echo "Revisa $@ para actualizarlo con respecto a $<"
@test -f $@ && diff -auN --color $@ $<
include .env
export
# XXX: El espacio antes del comentario cuenta como espacio
args ?=## Argumentos para Hain
commit ?= origin/rails## Commit desde el que actualizar
env ?= staging## Entorno del nodo delegado
sutty ?= $(SUTTY)## Dirección local
delegate ?= $(DELEGATE)## Cambia el nodo delegado
hain ?= ENV_FILE=.env $(HAINISH)## Ubicación de Hainish
# El nodo delegado tiene dos entornos, production y staging.
# Dependiendo del entorno que elijamos, se van a generar los assets y el
# contenedor y subirse a un servidor u otro. No utilizamos CI/CD (aún).
#
# Production es el entorno de panel.sutty.nl
ifeq ($(env),production)
container ?= panel
## TODO: Cambiar a otra cosa
branch ?= rails
public ?= public
endif
# Staging es el entorno de panel.staging.sutty.nl
ifeq ($(env),staging)
container := staging
branch := staging
public := staging
endif
help: always ## Ayuda
@echo -e "Sutty\n" | sed -re "s/^.*/\x1B[38;5;197m&\x1B[0m/"
@echo -e "Servidor: https://panel.$(SUTTY_WITH_PORT)/\n"
@echo -e "Uso: make TAREA args=\"ARGUMENTOS\"\n"
@echo -e "Tareas:\n"
@grep -E "^[a-z\-]+:.*##" Makefile | sed -re "s/(.*):.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
@echo -e "\nArgumentos:\n"
@grep -E "^[a-z\-]+ \?=.*##" Makefile | sed -re "s/(.*) \?=.*##(.*)/\1;\2/" | column -s ";" -t | sed -re "s/^([^ ]+) /\x1B[38;5;197m\1\x1B[0m/"
test: always ## Ejecutar los tests
$(MAKE) rake args="test RAILS_ENV=test $(args)"
postgresql: /etc/hosts ## Iniciar la base de datos
pgrep postgres >/dev/null || $(hain) postgresql
serve-js: /etc/hosts node_modules ## Iniciar el servidor de desarrollo de Javascript
$(hain) 'bundle exec ./bin/webpack-dev-server'
serve: /etc/hosts postgresql Gemfile.lock ## Iniciar el servidor de desarrollo de Rails
$(MAKE) rails args=server
rails: ## Corre rails dentro del entorno de desarrollo (pasar argumentos con args=).
$(MAKE) bundle args="exec rails $(args)"
rake: ## Corre rake dentro del entorno de desarrollo (pasar argumentos con args=).
$(MAKE) bundle args="exec rake $(args)"
bundle: ## Corre bundle dentro del entorno de desarrollo (pasar argumentos con args=).
$(hain) 'bundle $(args)'
psql := psql -h $(PG_HOST) -U $(PG_USER) -p $(PG_PORT) -d sutty
copy-table:
test -n "$(table)"
echo "truncate $(table) $(cascade);" | $(psql)
ssh $(delegate) docker exec postgresql pg_dump -U sutty -d sutty -t $(table) | $(psql)
psql:
$(psql)
rubocop: ## Yutea el código que está por ser commiteado
git status --porcelain \
| grep -E "^(A|M)" \
| sed "s/^...//" \
| grep ".rb$$" \
| ../haini.sh/haini.sh "xargs -r ./bin/rubocop --auto-correct"
audit: ## Encuentra dependencias con vulnerabilidades
$(hain) 'gem install bundler-audit'
$(hain) 'bundle audit --update'
brakeman: ## Busca posibles vulnerabilidades en Sutty
$(MAKE) bundle args='exec brakeman'
yarn: ## Tareas de yarn
$(hain) 'yarn $(args)'
clean: ## Limpieza
rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage
build: Gemfile.lock ## Generar la imagen Docker
time docker build --build-arg="BRANCH=$(branch)" --build-arg="RAILS_MASTER_KEY=`cat config/master.key`" -t sutty/$(container) .
docker tag sutty/$(container):latest sutty:keep
@echo -e "\a"
save: ## Subir la imagen Docker al nodo delegado
time docker save sutty/$(container):latest | ssh root@$(delegate) docker load
date +%F | xargs -I {} git tag -f $(container)-{}
@echo -e "\a"
ota: ## Actualizar Rails en el nodo delegado
git push
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl pull
ssh $(delegate) git -C /srv/sutty/srv/http/panel.sutty.nl lfs prune
ssh $(delegate) chown -R 1000:82 /srv/sutty/srv/http/panel.sutty.nl
ssh $(delegate) docker exec $(container) rails reload
# Correr un test en particular por ejemplo
# `make test/models/usuarie_test.rb`
tests := $(shell find test/ -name "*_test.rb")
$(tests): always
$(MAKE) test args="TEST=$@"
# Agrega las direcciones locales al sistema
/etc/hosts: always
@echo "Chequeando si es necesario agregar el dominio local $(SUTTY)"
@grep -q " $(SUTTY)$$" $@ || echo -e "127.0.0.1 $(SUTTY)\n::1 $(SUTTY)" | sudo tee -a $@
@grep -q " api.$(SUTTY)$$" $@ || echo -e "127.0.0.1 api.$(SUTTY)\n::1 api.$(SUTTY)" | sudo tee -a $@
@grep -q " panel.$(SUTTY)$$" $@ || echo -e "127.0.0.1 panel.$(SUTTY)\n::1 panel.$(SUTTY)" | sudo tee -a $@
@grep -q " postgresql.$(SUTTY)$$" $@ || echo -e "127.0.0.1 postgresql.$(SUTTY)\n::1 postgresql.$(SUTTY)" | sudo tee -a $@
# Instala las dependencias de Javascript
node_modules: package.json
$(MAKE) yarn
# Instala las dependencias de Rails
Gemfile.lock: Gemfile
$(MAKE) bundle args=install
.PHONY: always

View file

@ -7,5 +7,6 @@ blazer: bundle exec rake blazer:send_failing_checks
prometheus: bundle exec prometheus_exporter -b 0.0.0.0 --prefix "sutty_"
distributed_press_tokens_renew: bundle exec rake distributed_press:tokens:renew
cleanup: bundle exec rake cleanup:everything
emergency_cleanup: bundle exec rake cleanup:everything BEFORE=7
stats: bundle exec rake stats:process_all
que: daemonize -c /srv/ -p /srv/tmp/que.pid -u rails /usr/local/bin/syslogize bundle exec que

View file

@ -17,14 +17,32 @@ Para más información visita el [sitio de Sutty](https://sutty.nl/).
### Desarrollar
Todas las tareas se gestionan con `make`, por favor instala GNU Make
antes de comenzar.
Para facilitar la gestión de dependencias y entorno de desarrollo,
instala [haini.sh](https://0xacab.org/sutty/haini.sh)
Todas las tareas se gestionan con `go-task`. [Instrucciones de
instalación (en inglés)](https://taskfile.dev/installation/)
```bash
make help
go-task
```
[Leer la documentación](https://docs.sutty.nl/)
### Variables de entorno
Las variables de entorno por defecto se encuentran en el archivo `.env`.
Para modificar las opciones, crear o modificar el archivo `.env.local`
con valores distintos.
### Documentación
Para navegar la documentación del código usando YARD:
```bash
go-task doc serve
```
Y luego navegar a <https://panel.sutty.local:3000/doc/>
## English
@ -39,10 +57,29 @@ For more information, visit [Sutty's website](https://sutty.nl/en/).
### Development
Every task is run via `make`, please install GNU Make before developing.
To facilitate dependencies and dev environment, install
[haini.sh](https://0xacab.org/sutty/haini.sh)
Every task is run via `go-task`. [Installation
instructions](https://taskfile.dev/installation/).
```bash
make help
go-task
```
[Read the documentation](https://docs.sutty.nl/en/)
### Environment variables
Default env vars are store on `.env`. For local options, copy them to
`.env.local`.
### Documentation
To browse documentation using YARD:
```bash
go-task doc serve
```
And then open <https://panel.sutty.local:3000/doc/>

185
Taskfile.yaml Normal file
View file

@ -0,0 +1,185 @@
---
version: "3"
vars:
CURRENT_BRANCH:
sh: "git rev-parse --abbrev-ref HEAD"
shopt:
- "globstar"
dotenv:
- ".env.development"
- ".env"
- ".env.local"
- ".env.development.local"
tasks:
credentials:
desc: "Generate credentials file"
cmds:
- "cp --no-clobber config/credentials.yml.enc.ci config/credentials.yml.enc"
sources:
- "config/credentials.yml.enc.ci"
generates:
- "config/credentials.yml.enc"
gems:
desc: "Install gems"
deps:
- "credentials"
cmds:
- "{{.HAINISH}} bundle config set --local path './vendor'"
- "{{.HAINISH}} bundle install"
sources:
- "Gemfile"
generates:
- "Gemfile.lock"
status:
- "test -d vendor/ruby"
clean:
desc: "Clean"
cmds:
- "rm -rf _sites/test-* _deploy/test-* log/*.log tmp/cache tmp/letter_opener tmp/miniprofiler tmp/storage"
node-modules:
desc: "Install Node modules"
cmds:
- "{{.HAINISH}} yarn"
sources:
- "package.json"
- "yarn.lock"
status:
- "test -d node_modules"
assets:
desc: "Generate assets"
deps:
- "node-modules"
- "gems"
cmds:
- "git lfs fetch"
- "git lfs checkout"
- task: "rails"
vars:
CLI_ARGS: "webpacker:clobber RAILS_ENV=production"
- task: "rails"
vars:
CLI_ARGS: "assets:precompile RAILS_ENV=production"
- task: "rails"
vars:
CLI_ARGS: "assets:clean RAILS_ENV=production"
sources:
- "package.json"
- "yarn.lock"
- "app/assets/**/*"
- "app/javascript/**/*"
generates:
- "public/packs/manifest.json"
hosts:
desc: "Local DNS resolution for hostnames"
interactive: true
cmds:
- "echo -e \"127.0.0.1 panel.{{.SUTTY}} api.{{.SUTTY}} postgresql.{{.SUTTY}}\" | sudo tee -a /etc/hosts"
- "echo -e \"::1 panel.{{.SUTTY}} api.{{.SUTTY}} postgresql.{{.SUTTY}}\" | sudo tee -a /etc/hosts"
status:
- "grep -q \" panel.{{.SUTTY}} \" /etc/hosts"
database-init:
desc: "Database install"
cmds:
- "{{.HAINISH}} /usr/bin/initdb --locale en_US.utf8 -E UTF8 -D /var/lib/postgresql/{{.PGVER}}/data"
- "echo \"host all all samenet trust\" >> ../hain/var/lib/postgresql/{{.PGVER}}/data/pg_hba.conf"
- "echo \"listen_addresses = '*'\" >> ../hain/var/lib/postgresql/{{.PGVER}}/data/postgresql.conf"
- "echo \"external_pid_file = '{{.PGPID}}'\" >> ../hain/var/lib/postgresql/{{.PGVER}}/data/postgresql.conf"
- "install -dm755 ../hain/run/postgresql"
status:
- "test -d ../hain/var/lib/postgresql/{{.PGVER}}/data"
- "test -f ../hain/var/lib/postgresql/{{.PGVER}}/data/postgresql.conf"
database:
desc: "Database"
deps:
- "database-init"
cmds:
- "{{.HAINISH}} daemonize -c /var/lib/postgresql/{{.PGVER}}/data /usr/bin/postgres -D /var/lib/postgresql/{{.PGVER}}/data"
status:
- "test -f ../hain{{.PGPID}}"
- "pgrep -F ../hain{{.PGPID}}"
prepare:
desc: "Create database or run pending migrations"
deps:
- "database"
cmds:
- task: "rails"
vars:
CLI_ARGS: "db:prepare"
serve:
desc: "Run Rails development server"
deps:
- "prepare"
- "gems"
cmds:
- ": == Development server running at https://panel.{{.SUTTY_WITH_PORT}} =="
- task: "rails"
vars:
CLI_ARGS: "server"
status:
- "test -f tmp/pids/server.pid"
- "pgrep -F tmp/pids/server.pid"
yarn:
desc: "Yarn. Call with: go-task yarn -- arguments"
deps:
- "node-modules"
cmds:
- "{{.HAINISH}} yarn {{.CLI_ARGS}}"
- defer:
task: "notify"
bundle:
desc: "Bundle. Call with: go-task bundle -- arguments"
interactive: true
deps:
- "gems"
cmds:
- "{{.HAINISH}} bundle {{.CLI_ARGS}}"
- defer:
task: "notify"
rails:
desc: "Rails. Call with: go-task rails -- arguments"
cmds:
- task: "bundle"
vars:
CLI_ARGS: "exec rails {{.CLI_ARGS}}"
console:
desc: "Rails console"
interactive: true
cmds:
- task: "rails"
vars:
CLI_ARGS: "console"
doc:
desc: "Build documentation"
deps:
- "gems"
cmds:
- task: "bundle"
vars:
CLI_ARGS: "exec yardoc -o public/doc app lib config db"
gem-audit:
desc: "Audit Gem dependencies"
deps:
- "gems"
- "bundler-audit"
cmds:
- task: "bundle"
vars:
CLI_ARGS: "audit --update"
node-audit:
desc: "Audit Node dependencies"
deps:
- "node-modules"
cmds:
- task: "yarn"
vars:
CLI_ARGS: "audit"
notify:
internal: true
cmds:
- "echo -e \"\a\""
bundler-audit:
internal: true
cmds:
- "{{.HAINISH}} gem install bundler-audit"
status:
- "test -f ../hain/usr/bin/bundler-audit"

View file

@ -29,11 +29,6 @@ $sizes: (
"70ch": 70ch,
);
.btn {
background-color: var(--foreground);
color: var(--background);
}
@import "bootstrap";
@import "editor";
@ -57,6 +52,25 @@ $sizes: (
--background: #{$black};
--color: #{$cyan};
}
.btn-secondary {
background-color: $white;
color: $black;
border: none;
&:hover {
color: $black;
background-color: $cyan;
}
&:active {
background-color: $cyan;
}
&:focus {
box-shadow: 0 0 0 0.2rem $cyan;
}
}
}
// TODO: Encontrar la forma de generar esto desde los locales de Rails
@ -195,7 +209,7 @@ fieldset {
&[type=button] {
@extend .btn;
@extend .btn-info;
@extend .btn-secondary;
@extend .m-0;
}
}
@ -209,8 +223,6 @@ svg {
}
.btn {
border: none;
border-radius: 0;
margin-right: 0.3rem;
margin-bottom: 0.3rem;
@ -246,7 +258,7 @@ svg {
color: $magenta;
}
.btn {
.btn-secondary {
background-color: $white;
color: $black;
border: none;
@ -525,3 +537,43 @@ $bezier: cubic-bezier(0.75, 0, 0.25, 1);
}
}
}
@import "new_editor";
.new-editor {
.editor {
table {
@extend .table;
@extend .table-responsive;
}
}
}
.content {
p { min-height: $font-size-base * $line-height-base; }
h1 { min-height: $h1-font-size * $headings-line-height; }
h2 { min-height: $h2-font-size * $headings-line-height; }
h3 { min-height: $h3-font-size * $headings-line-height; }
h4 { min-height: $h4-font-size * $headings-line-height; }
h5 { min-height: $h5-font-size * $headings-line-height; }
h6 { min-height: $h6-font-size * $headings-line-height; }
iframe { border: 0; }
audio { width: 100%; }
img,
video,
iframe {
@extend .img-fluid;
height: auto;
}
& > * {
margin-bottom: 1rem;
&:last-child {
margin-bottom: 0;
}
}
}

View file

@ -0,0 +1,28 @@
$black: black;
$white: white;
$cyan: #13fefe;
:root {
--foreground: #{$white};
--background: #{$black};
--color: #{$cyan};
}
.btn-secondary {
background-color: $white;
color: $black;
border: none;
&:hover {
color: $black;
background-color: $cyan;
}
&:active {
background-color: $cyan;
}
&:focus {
box-shadow: 0 0 0 0.2rem $cyan;
}
}

View file

@ -1,4 +1,4 @@
.editor {
.old.editor {
box-sizing: border-box;
*, *::before, *::after { box-sizing: inherit; }

View file

@ -0,0 +1,22 @@
.new-editor {
.editor {
.menubar {
z-index: 1;
label.btn {
margin-bottom: 0.3rem !important;
}
.btn-group {
.btn {
margin-right: 0;
}
}
}
.ProseMirror,
& > ol li {
margin-bottom: 1rem;
}
}
}

View file

@ -18,8 +18,9 @@ module ActiveStorage
# para que puedan propagarse correctamente a través de todo el
# stack.
def blob_args
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type, metadata: {}).to_h.symbolize_keys.tap do |ba|
ba[:filename] = ba[:filename].unicode_normalize
params.require(:blob).permit(:filename, :byte_size, :checksum, :content_type,
metadata: {}).to_h.symbolize_keys.tap do |ba|
ba[:filename] = ba[:filename].unicode_normalize.sub(/\A_+/, '')
end
end
end

View file

@ -6,16 +6,45 @@ module ActiveStorage
extend ActiveSupport::Concern
included do
alias_method :original_show, :show
# Permitir incrustar archivos subidos (especialmente PDFs) desde
# otros sitios.
def show
original_show.tap do |s|
response.headers.delete 'X-Frame-Options'
end
end
rescue_from ActiveRecord::RecordNotFound, with: :page_not_found
# Asociar el archivo subido al sitio correspondiente. Cada sitio
# tiene su propio servicio de subida de archivos.
def update
if (token = decode_verified_token)
if acceptable_content?(token)
named_disk_service(token[:service_name]).upload token[:key], request.body, checksum: token[:checksum]
blob = ActiveStorage::Blob.find_by_key token[:key]
blob = ActiveStorage::Blob.find_by_key! token[:key]
site = Site.find_by_name token[:service_name]
if remote_file?(token)
begin
url = request.body.read
body = Down.download(url, max_size: 111.megabytes)
checksum = Digest::MD5.file(body.path).base64digest
blob.metadata[:url] = url
blob.update_columns checksum: checksum, byte_size: body.size, metadata: blob.metadata
rescue StandardError => e
ExceptionNotifier.notify_exception(e, data: { key: token[:key], url: url, site: site.name })
head :content_too_large
end
else
body = request.body
checksum = token[:checksum]
end
named_disk_service(token[:service_name]).upload token[:key], body, checksum: checksum
site.static_files.attach(blob)
else
head :unprocessable_entity
@ -26,6 +55,17 @@ module ActiveStorage
rescue ActiveStorage::IntegrityError
head :unprocessable_entity
end
private
def remote_file?(token)
token[:content_type] == 'sutty/download-from-url'
end
def page_not_found(exception)
head :not_found
ExceptionNotifier.notify_exception(exception, data: {params: params.to_hash})
end
end
end
end

View file

@ -12,7 +12,7 @@ module Api
def create
if (site&.airbrake_valid? airbrake_token) && !detected_device.bot?
BacktraceJob.perform_later site_id: params[:site_id],
params: airbrake_params.to_h
params: airbrake_params.to_h
end
render status: 201, json: { id: 1, url: '' }
@ -23,7 +23,39 @@ module Api
# XXX: Por alguna razón Airbrake envía los datos con Content-Type:
# text/plain.
def airbrake_params
@airbrake_params ||= params.merge!(FastJsonparser.parse(request.raw_post) || {}).permit!
@airbrake_params ||=
params.merge!(FastJsonparser.parse(request.raw_post) || {})
.permit(
{
errors: [
:type,
:message,
{ backtrace: %i[file line column function] }
]
},
{
context: [
:url,
:language,
:severity,
:userAgent,
:windowError,
:rootDirectory,
{
history: [
:date,
:type,
:severity,
:target,
:method,
:duration,
:statusCode,
{ arguments: [] }
]
}
]
}
)
end
def site

View file

@ -9,7 +9,7 @@ module Api
# Lista de nombres de dominios a emitir certificados
def index
render json: sites_names + alternative_names + api_names + www_names
render json: alternative_names + api_names + www_names
end
private
@ -18,17 +18,16 @@ module Api
name.end_with?('.') ? name[0..-2] : "#{name}.#{Site.domain}"
end
# Nombres de los sitios
def sites_names
Site.all.order(:name).pluck(:name).map do |name|
canonicalize name
end
def subdomain?(name)
name.end_with? ".#{Site.domain}"
end
# Dominios alternativos
def alternative_names
(DeployAlternativeDomain.all.map(&:hostname) + DeployLocalizedDomain.all.map(&:hostname)).map do |name|
canonicalize name
end.reject do |name|
subdomain? name
end
end
@ -41,6 +40,8 @@ module Api
.or(Site.where(colaboracion_anonima: true))
.select("'api.' || name as name").map(&:name).map do |name|
canonicalize name
end.reject do |name|
subdomain? name
end
end

View file

@ -0,0 +1,77 @@
# frozen_string_literal: true
module Api
module V1
# Recibe webhooks y lanza un PullJob
class WebhooksController < BaseController
# responde con forbidden si falla la validación del token
rescue_from ActiveRecord::RecordNotFound, with: :platforms_answer
# Trae los cambios a partir de un post de Webhooks:
# (Gitlab, Github, Gitea, etc)
#
# @return [nil]
def pull
message = I18n.with_locale(site.default_locale) do
I18n.t('webhooks.pull.message')
end
GitPullJob.perform_later(site, usuarie, message)
head :ok
end
private
# encuentra el sitio a partir de la url
def site
@site ||= Site.find_by_name!(params[:site_id])
end
# valida el token que envía la plataforma del webhook
#
# @return [String]
def token
@token ||=
begin
# Gitlab
if request.headers['X-Gitlab-Token'].present?
request.headers['X-Gitlab-Token']
# Github
elsif request.headers['X-Hub-Signature-256'].present?
token_from_signature(request.headers['X-Hub-Signature-256'], 'sha256=')
# Gitea
elsif request.headers['X-Gitea-Signature'].present?
token_from_signature(request.headers['X-Gitea-Signature'])
else
raise ActiveRecord::RecordNotFound, 'proveedor no soportado'
end
end
end
# valida token a partir de firma de webhook
#
# @return [String, Boolean]
def token_from_signature(signature, prepend = '')
payload = request.body.read
site.roles.where(temporal: false, rol: 'usuarie').pluck(:token).find do |token|
new_signature = prepend + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), token, payload)
ActiveSupport::SecurityUtils.secure_compare(new_signature, signature.to_s)
end.tap do |t|
raise ActiveRecord::RecordNotFound, 'token no encontrado' if t.nil?
end
end
# encuentra le usuarie
def usuarie
@usuarie ||= site.roles.find_by!(temporal: false, rol: 'usuarie', token: token).usuarie
end
# respuesta de error a plataformas
def platforms_answer(exception)
ExceptionNotifier.notify_exception(exception, data: { headers: request.headers.to_h })
head :forbidden
end
end
end
end

View file

@ -59,7 +59,11 @@ class ApplicationController < ActionController::Base
#
# @return [String,Symbol]
def current_locale
session[:locale] = params[:change_locale_to] if params[:change_locale_to].present?
locale = params[:change_locale_to]
if locale.present? && I18n.locale_available?(locale)
session[:locale] = params[:change_locale_to]
end
session[:locale] || current_usuarie&.lang || I18n.locale
end

View file

@ -110,27 +110,6 @@ class SitesController < ApplicationController
redirect_to sites_path
end
# Obtiene y streamea archivos estáticos desde el repositorio mismo,
# pero sólo los públicos (es decir los archivos subidos desde Sutty).
def static_file
authorize site
file = params.require(:file) + '.' + params.require(:format)
raise ActionController::RoutingError.new(nil, nil) unless file.start_with? 'public/'
path = site.relative_path file
raise ActionController::RoutingError.new(nil, nil) unless File.exist? path
# TODO: Hacer esto usa recursos, pero menos que generar el sitio
# cada vez. Para poder usar X-Accel tendríamos que montar los
# repositorios en el servidor web, cosa que no queremos, o hacer
# links simbólicos desde todos los public, o usar un servidor web
# local que soporte sendfile mejor que Rails (nghttpd?)
send_file path
end
private
def site

View file

@ -6,3 +6,4 @@ import './prosemirror'
import './timezone'
import './turbolinks-anchors'
import './validation'
import './new_editor'

View file

@ -0,0 +1,14 @@
import SuttyEditor from "@suttyweb/editor";
import "@suttyweb/editor/dist/style.css";
document.addEventListener("turbolinks:load", () => {
document.querySelectorAll(".new-editor").forEach((editorContainer) => {
new SuttyEditor({
target: editorContainer,
props: {
textareaEl: editorContainer.querySelector("textarea"),
},
});
});
});

8
app/jobs/cleanup_job.rb Normal file
View file

@ -0,0 +1,8 @@
# frozen_string_literal: true
# Realiza tareas de limpieza en segundo plano
class CleanupJob < ApplicationJob
def perform(before = nil)
CleanupService.new(before: before).cleanup_everything!
end
end

View file

@ -56,6 +56,10 @@ class DeployJob < ApplicationJob
rescue URI::Error
nil
end.compact
if d == @site.deployment_list.last && !status
raise DeployException, 'Falló la compilación'
end
rescue StandardError => e
status = false
seconds ||= 0

16
app/jobs/git_pull_job.rb Normal file
View file

@ -0,0 +1,16 @@
# frozen_string_literal: true
# Permite traer los cambios desde webhooks
class GitPullJob < ApplicationJob
# @param :site [Site]
# @param :usuarie [Usuarie]
# @return [nil]
def perform(site, usuarie)
return unless site.repository.origin
return unless site.repository.fetch.positive?
site.repository.merge(usuarie)
site.reindex_changes!
end
end

View file

@ -30,7 +30,7 @@ class GitlabNotifierJob < ApplicationJob
count: 1,
issue: @issue['iid'],
user_agents: [user_agent].compact,
params: [request&.filtered_parameters].compact,
params: request&.filtered_parameters&.as_json,
urls: [url].compact
}
end
@ -105,7 +105,7 @@ class GitlabNotifierJob < ApplicationJob
def title
@title ||= ''.dup.tap do |t|
t << "[#{exception.class}] " unless javascript?
t << exception.message
t << exception.message[0..200]
t << " [#{issue_data[:count]}]"
end
end
@ -192,7 +192,7 @@ class GitlabNotifierJob < ApplicationJob
```
#{request.request_method} #{url}
#{pp request.filtered_parameters}
#{pp request.filtered_parameters.as_json}
```
REQUEST

View file

@ -11,7 +11,7 @@ module ActionDispatch
# Devolver el nombre de archivo con caracteres unicode
# normalizados
def original_filename
@original_filename.unicode_normalize
@original_filename.unicode_normalize.sub(/\A_+/, '')
end
end
end

View file

@ -96,7 +96,7 @@ module ActiveStorage
end
def blob_for(key)
ActiveStorage::Blob.find_by(key: key, service_name: name)
ActiveStorage::Blob.find_by!(key: key, service_name: name)
end
end
end

View file

@ -0,0 +1,36 @@
# frozen_string_literal: true
module Jekyll
module Readers
# Permite leer datos utilizando rutas absolutas.
#
# {Jekyll::DataReader} usa {Dir.chdir} con rutas relativas, lo que
# en nuestro uso provoca confusiones en el lector de datos.
#
# Con este módulo, podemos leer todos los archivos usando rutas
# absolutas, lo que nos permite reemplazar jekyll-data, que agregaba
# código duplicado.
module DataReaderDecorator
extend ActiveSupport::Concern
included do
def read_data_to(dir, data)
return unless File.directory?(dir) && !@entry_filter.symlink?(dir)
Dir.glob(File.join(dir, '*')).each do |path|
next if @entry_filter.symlink?(path)
entry = Pathname.new(path).relative_path_from(dir).to_s
if File.directory?(path)
read_data_to(path, data[sanitize_filename(entry)] = {})
else
key = sanitize_filename(File.basename(entry, ".*"))
data[key] = read_data_file(path)
end
end
end
end
end
end
end

View file

@ -1,7 +1,6 @@
# frozen_string_literal: true
require 'distributed_press/v1/client/site'
require 'njalla/v1'
# Soportar Distributed Press APIv1
#
@ -15,8 +14,8 @@ require 'njalla/v1'
class DeployDistributedPress < Deploy
store :values, accessors: %i[hostname remote_site_id remote_info], coder: JSON
before_create :create_remote_site!, :create_njalla_records!
before_destroy :delete_remote_site!, :delete_njalla_records!
before_create :create_remote_site!
before_destroy :delete_remote_site!
DEPENDENCIES = %i[deploy_local]
@ -31,17 +30,12 @@ class DeployDistributedPress < Deploy
time_start
create_remote_site! if remote_site_id.blank?
create_njalla_records!
save
if remote_site_id.blank?
raise DeployJob::DeployException, 'El sitio no se creó en Distributed Press'
end
if create_njalla_records? && remote_info[:njalla].blank?
raise DeployJob::DeployException, 'No se pudieron crear los registros necesarios en Njalla'
end
site_client.tap do |c|
stdout = Thread.new(publisher.logger_out) do |io|
until io.eof?
@ -52,7 +46,12 @@ class DeployDistributedPress < Deploy
end
end
status = c.publish(publishing_site, deploy_local.destination)
begin
status = c.publish(publishing_site, deploy_local.destination)
rescue DistributedPress::V1::Error => e
ExceptionNotifier.notify_exception(e, data: { site: site.name })
status = false
end
if status
self.remote_info[:distributed_press] = c.show(publishing_site).to_h
@ -129,6 +128,8 @@ class DeployDistributedPress < Deploy
#
# @return [nil]
def create_remote_site!
self.hostname ||= site.hostname
created_site = site_client.create(create_site)
self.remote_site_id = created_site[:id]
@ -140,29 +141,6 @@ class DeployDistributedPress < Deploy
nil
end
# Crea los registros en Njalla
#
# XXX: Esto depende de nuestro DNS actual, cuando lo migremos hay
# que eliminarlo.
#
# @return [nil]
def create_njalla_records!
return unless create_njalla_records?
self.remote_info ||= {}
self.remote_info[:njalla] ||= {}
self.remote_info[:njalla][:a] ||= njalla.add_record(name: site.name, type: 'CNAME', content: "#{Site.domain}.").to_h
self.remote_info[:njalla][:cname] ||= njalla.add_record(name: "www.#{site.name}", type: 'CNAME', content: "#{Site.domain}.").to_h
self.remote_info[:njalla][:ns] ||= njalla.add_record(name: "_dnslink.#{site.name}", type: 'NS', content: "#{publisher.hostname}.").to_h
nil
rescue HTTParty::Error => e
ExceptionNotifier.notify_exception(e, data: { site: site.name })
self.remote_info.delete :njalla
ensure
nil
end
# Registra lo que sucedió
#
# @param status [Bool]
@ -180,31 +158,4 @@ class DeployDistributedPress < Deploy
ExceptionNotifier.notify_exception(e, data: { site: site.name })
nil
end
def delete_njalla_records!
return unless create_njalla_records?
%w[a ns cname].each do |type|
next if (id = remote_info.dig('njalla', type, 'id')).blank?
njalla.remove_record(id: id.to_i)
end
end
# Actualizar registros en Njalla
#
# @return [Njalla::V1::Domain]
def njalla
@njalla ||=
begin
client = Njalla::V1::Client.new(token: Rails.application.credentials.njalla)
Njalla::V1::Domain.new(domain: Site.domain, client: client)
end
end
# Detecta si tenemos que crear registros en Njalla
def create_njalla_records?
!site.name.end_with?('.')
end
end

View file

@ -7,6 +7,20 @@ class DeployLocal < Deploy
before_destroy :remove_destination!
def bundle(output: false)
run %(bundle config set --local clean 'true'), output: output
run(%(bundle config set --local deployment 'true'), output: output) if site.gemfile_lock_path?
run %(bundle config set --local path '#{gems_dir}'), output: output
run %(bundle config set --local without 'test development'), output: output
run %(bundle config set --local cache_all 'false'), output: output
run %(bundle install), output: output
end
def git_lfs(output: false)
run %(git lfs fetch), output: output
run %(git lfs checkout), output: output
end
# Realizamos la construcción del sitio usando Jekyll y un entorno
# limpio para no pasarle secretos
#
@ -55,7 +69,7 @@ class DeployLocal < Deploy
#
# @return [nil]
def cleanup!
FileUtils.rm_rf(gems_dir)
FileUtils.rm_rf(site.bundle_path)
FileUtils.rm_rf(yarn_cache_dir)
FileUtils.rm_rf(File.join(site.path, 'node_modules'))
FileUtils.rm_rf(File.join(site.path, '.sass-cache'))
@ -106,9 +120,11 @@ class DeployLocal < Deploy
File.exist? pnpm_lock
end
def git_lfs(output: false)
run %(git lfs fetch), output: output
run %(git lfs checkout), output: output
def pnpm(output: false)
return true unless pnpm_lock?
run %(pnpm config set store-dir "#{pnpm_cache_dir}"), output: output
run 'pnpm install --production', output: output
end
def gem(output: false)
@ -122,17 +138,6 @@ class DeployLocal < Deploy
run 'yarn install --production', output: output
end
def pnpm(output: false)
return true unless pnpm_lock?
run %(pnpm config set store-dir "#{pnpm_cache_dir}"), output: output
run 'pnpm install --production', output: output
end
def bundle(output: false)
run %(bundle install --deployment --no-cache --path="#{gems_dir}" --clean --without test development), output: output
end
def jekyll_build(output: false)
with_tempfile(site.private_key_pem) do |file|
flags = extra_flags(private_key: file)

View file

@ -36,6 +36,15 @@ class IndexedPost < ApplicationRecord
belongs_to :site
# Encuentra el post original
#
# @return [nil,Post]
def post
return if post_id.blank?
@post ||= site.posts(lang: locale).find(post_id, uuid: true)
end
# Convertir locale a direccionario de PG
#
# @param [String,Symbol]

View file

@ -24,7 +24,11 @@ class MetadataContent < MetadataTemplate
end
def to_s
sanitizer.sanitize value, tags: [], attributes: []
Nokogiri::HTML5.fragment(value).tap do |html|
html.css('[src^="public/"]').each do |element|
element['src'] = convert_internal_path_to_src element['src']
end
end.to_s
end
private
@ -42,21 +46,22 @@ class MetadataContent < MetadataTemplate
# TODO: En lugar de comprobar el Content Type acá, restringir los
# tipos de archivo a aceptar en ActiveStorage.
def sanitize(html_string)
html = Nokogiri::HTML.fragment(super html_string)
html = Nokogiri::HTML5.fragment(super html_string)
elements = 'img,audio,video,iframe'
# Eliminar elementos sin src y comprobar su origen
html.css(elements).each do |element|
unless element['src']
element.remove
next
end
begin
raise URI::Error unless element['src'].present?
uri = URI element['src']
# No permitimos recursos externos
element.remove unless uri.scheme == 'https' && uri.hostname.end_with?(Site.domain)
raise URI::Error unless Rails.application.config.hosts.include?(uri.hostname)
element['src'] = convert_src_to_internal_path uri
raise URI::Error if element['src'].blank?
rescue URI::Error
element.remove
end
@ -73,16 +78,74 @@ class MetadataContent < MetadataTemplate
end
# Elimina los estilos salvo los que asigne el editor
html.css('*').each do |element|
next if elements_with_style.include? element.name.downcase
element.remove_attribute('style')
html.css('[style]').each do |element|
if (style = sanitize_style(element['style'])).present?
element['style'] = style
else
element.remove_attribute('style')
end
end
html.to_s.html_safe
end
def elements_with_style
@elements_with_style ||= %w[div mark].freeze
# Limpia estilos en base a una lista de permitidos
#
# @param style [String]
# @return [String]
def sanitize_style(style)
style.split(';').reduce({}) do |style_hash, style_string|
key, value = style_string.split(':', 2)
style_hash[key] ||= value
style_hash
end.slice(*allowed_styles).map do |style_pair|
style_pair.join(':')
end.join(';')
end
# Estilos permitidos
#
# @return [Array<String>]
def allowed_styles
@allowed_styles ||= %w[text-align color background-color]
end
# Convierte una ubicación local al sitio en una URL de ActiveStorage
#
# XXX: Por qué son tan díficiles de encontrar las rutas de AS
#
# @param path [String]
# @return [String]
def convert_internal_path_to_src(path)
key = path.split('/').second
blob = ActiveStorage::Blob.find_by(service_name: site.name, key: key)
return unless blob
"/rails/active_storage/blobs/#{blob.signed_id}/#{blob.filename}"
end
# Convierte una URI en una ruta interna del sitio actual
#
# XXX: No verifica si el archivo existe o no. Se supone que existe
# porque ya fue subido antes.
#
# @param uri [URI]
# @return [String,nil]
def convert_src_to_internal_path(uri)
signed_id = uri.path.split('/').fifth
blob = ActiveStorage::Blob.find_signed(signed_id)
return unless blob
return unless blob.service_name == site.name
blob_path = Pathname.new(blob.service.path_for(blob.key)).realpath
site_path = Pathname.new(site.path).realpath
blob_path.relative_path_from(site_path).to_s
rescue ActiveSupport::MessageVerifier::InvalidSignature => e
ExceptionNotifier.notify_exception(e, data: { site: site.name })
nil
end
end

View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
# Testear el nuevo editor
class MetadataNewContent < MetadataContent; end

View file

@ -0,0 +1,4 @@
# frozen_string_literal: true
# Campos en HTML con el nuevo editor
class MetadataNewHtml < MetadataHtml; end

View file

@ -6,7 +6,7 @@ class MetadataPath < MetadataTemplate
#
# @return [String]
def default_value
File.join(site.path, "_#{lang}", "#{date}-#{slug}#{ext}")
File.join(site.path, "_#{lang}", "#{limited_name}#{ext}")
end
# La ruta del archivo según Jekyll
@ -46,4 +46,12 @@ class MetadataPath < MetadataTemplate
def date
post.date.value.strftime('%F')
end
# Limita el nombre de archivo a 255 bytes, de forma que siempre
# podemos guardarlo
#
# @return [String]
def limited_name
"#{date}-#{slug}".mb_chars.limit(255 - ext.length)
end
end

View file

@ -34,7 +34,7 @@ class MetadataRelatedPosts < MetadataArray
end
def title(post)
"#{post&.title&.value || post&.slug&.value} (#{post.layout.humanized_name})"
"#{post&.title&.value || post&.slug&.value} #{post&.date&.value.strftime('%F')} (#{post.layout.humanized_name})"
end
# Encuentra el filtro

View file

@ -202,12 +202,12 @@ MetadataTemplate = Struct.new(:site, :document, :name, :label, :type,
def allowed_attributes
@allowed_attributes ||= %w[style href src alt controls data-align data-multimedia data-multimedia-inner id
name].freeze
name rel target referrerpolicy class colspan rowspan role data-turbo start type reversed].freeze
end
def allowed_tags
@allowed_tags ||= %w[strong em del u mark p h1 h2 h3 h4 h5 h6 ul ol li img iframe audio video div figure blockquote
figcaption a sub sup small].freeze
figcaption a sub sup small table thead tbody tfoot tr th td br code].freeze
end
# Decifra el valor

View file

@ -103,8 +103,10 @@ class Post
src = element.attributes['src']
next unless src&.value&.start_with? 'public/'
file = MetadataFile.new(site: site, post: self, document: document, layout: layout)
file.value['path'] = src.value
src.value = Rails.application.routes.url_helpers.site_static_file_url(site, file: src.value)
src.value = Rails.application.routes.url_helpers.url_for(file.static_file)
end
# Notificar a les usuaries que están viendo una previsualización
@ -270,7 +272,7 @@ class Post
yaml['layout'] = layout.name.to_s
yaml['uuid'] = uuid.value
# Y que no se procese liquid
yaml['liquid'] = false
yaml['render_with_liquid'] = false
yaml['usuaries'] = usuaries.map(&:id).uniq
yaml['created_at'] = created_at.value
yaml['last_modified_at'] = modified_at

View file

@ -14,6 +14,8 @@ class Rol < ApplicationRecord
validates_inclusion_of :rol, in: ROLES
before_save :add_token_if_missing!
def invitade?
rol == INVITADE
end
@ -25,4 +27,11 @@ class Rol < ApplicationRecord
def self.role?(rol)
ROLES.include? rol
end
private
# Asegurarse que tenga un token
def add_token_if_missing!
self.token ||= SecureRandom.hex(64)
end
end

View file

@ -158,19 +158,19 @@ class Site < ApplicationRecord
# Traer la ruta del sitio
def path
File.join(Site.site_path, name)
::File.join(Site.site_path, name)
end
# La ruta anterior
def path_was
File.join(Site.site_path, name_was)
::File.join(Site.site_path, name_was)
end
# Limpiar la ruta y unirla con el separador de directorios del
# sistema operativo. Como si algún día fuera a cambiar o
# soportáramos Windows :P
def relative_path(suspicious_path)
File.join(path, *suspicious_path.gsub('..', '/').gsub('./', '').squeeze('/').split('/'))
::File.join(path, *suspicious_path.gsub('..', '/').gsub('./', '').squeeze('/').split('/'))
end
# Obtiene la lista de traducciones actuales
@ -208,10 +208,8 @@ class Site < ApplicationRecord
# Trae los datos del directorio _data dentro del sitio
def data
unless jekyll.data.present?
run_in_path do
jekyll.reader.read_data
jekyll.data['layouts'] ||= {}
end
jekyll.reader.read_data
jekyll.data['layouts'] ||= {}
end
jekyll.data
@ -221,9 +219,7 @@ class Site < ApplicationRecord
# colecciones.
def collections
unless @read
run_in_path do
jekyll.reader.read_collections
end
jekyll.reader.read_collections
@read = true
end
@ -318,9 +314,7 @@ class Site < ApplicationRecord
#
# @return [Hash]
def theme_layouts
run_in_path do
jekyll.reader.read_layouts
end
jekyll.reader.read_layouts
end
# Trae todos los valores disponibles para un campo
@ -363,7 +357,7 @@ class Site < ApplicationRecord
end
def jekyll?
File.directory? path
::File.directory? path
end
def jekyll
@ -371,9 +365,7 @@ class Site < ApplicationRecord
begin
install_gems
Jekyll::Site.new(configuration).tap do |site|
site.reader = JekyllData::Reader.new(site) if site.theme
end
Jekyll::Site.new(configuration)
end
end
@ -383,7 +375,7 @@ class Site < ApplicationRecord
# documentos de Jekyll hacia Sutty para que podamos leer los datos que
# necesitamos.
def load_jekyll
return unless name.present? && File.directory?(path)
return unless name.present? && ::File.directory?(path)
reload_jekyll!
end
@ -411,7 +403,7 @@ class Site < ApplicationRecord
# metadatos de Document
@configuration =
::Jekyll.configuration('source' => path,
'destination' => File.join(path, '_site'),
'destination' => ::File.join(path, '_site'),
'safe' => true, 'watch' => false,
'quiet' => true, 'excerpt_separator' => '')
@ -436,7 +428,7 @@ class Site < ApplicationRecord
# El directorio donde se almacenan los sitios
def self.site_path
@site_path ||= File.realpath(ENV.fetch('SITE_PATH', Rails.root.join('_sites')))
@site_path ||= ::File.realpath(ENV.fetch('SITE_PATH', Rails.root.join('_sites')))
end
def self.default
@ -461,6 +453,15 @@ class Site < ApplicationRecord
@docs = nil
end
# @return [Pathname]
def bundle_path
@bundle_path ||= Rails.root.join('_storage', 'gems', name)
end
def gemfile_lock_path?
::File.exist? gemfile_lock_path
end
private
# Asegurarse que el sitio tenga una llave privada
@ -473,7 +474,7 @@ class Site < ApplicationRecord
def clone_skel!
return if jekyll?
Rugged::Repository.clone_at ENV['SKEL_SUTTY'], path
Rugged::Repository.clone_at(ENV['SKEL_SUTTY'], path, checkout_branch: design.gem)
# Necesita un bloque
repository.rugged.remotes.rename('origin', 'upstream') {}
@ -494,8 +495,8 @@ class Site < ApplicationRecord
config.theme = design.gem unless design.no_theme?
config.description = description
config.title = title
config.url = url(slash: false)
config.hostname = hostname
config.url ||= url(slash: false)
config.hostname ||= hostname
config.locales = locales.map(&:to_s)
end
@ -564,26 +565,52 @@ class Site < ApplicationRecord
def install_gems
return unless persisted?
deploys.find_by_type('DeployLocal').send(:git_lfs)
deploy_local = deploys.find_by_type('DeployLocal')
deploy_local.git_lfs
if !gem_dir? || gemfile_updated? || gemfile_lock_updated?
deploys.find_by_type('DeployLocal').send(:bundle)
if !gems_installed? || gemfile_updated? || gemfile_lock_updated?
deploy_local.bundle
touch
FileUtils.touch(gemfile_path)
end
end
def gem_path
@gem_path ||=
begin
ruby_version = Gem::Version.new(RUBY_VERSION)
ruby_version.canonical_segments[2] = 0
bundle_path.join('ruby', ruby_version.canonical_segments.join('.'))
end
end
# Detecta si el repositorio de gemas existe
def gem_dir?
Rails.root.join('_storage', 'gems', name).directory?
def gems_installed?
gem_path.directory? && !gem_path.empty?
end
# Detecta si el Gemfile fue modificado
def gemfile_updated?
updated_at < File.mtime(File.join(path, 'Gemfile'))
updated_at < ::File.mtime(gemfile_path)
end
# Detecta si el Gemfile.lock fue modificado
def gemfile_path
@gemfile_path ||= ::File.join(path, 'Gemfile')
end
# @return [String]
def gemfile_lock_path
@gemfile_lock_path ||= ::File.join(path, 'Gemfile.lock')
end
# Detecta si el Gemfile.lock fue modificado con respecto al sitio o al
# Gemfile.
def gemfile_lock_updated?
updated_at < File.mtime(File.join(path, 'Gemfile.lock'))
return false unless gemfile_lock_path?
[updated_at, ::File.mtime(::File.join(path, 'Gemfile'))].any? do |compare|
compare < ::File.mtime(gemfile_lock_path)
end
end
end

View file

@ -21,7 +21,7 @@ class Site
# Leer el archivo de configuración y setear los atributos en el
# objeto actual, creando los metodos de ostruct
def read
data = YAML.safe_load(File.read(path))
data = YAML.safe_load(File.read(path), permitted_classes: [Time])
@hash = data.hash
data.each do |key, value|

View file

@ -1,22 +1,125 @@
# frozen_string_literal: true
# Indexa todos los artículos de un sitio
#
# TODO: Hacer opcional
class Site
# Indexa todos los artículos de un sitio
#
# TODO: Hacer opcional
module Index
extend ActiveSupport::Concern
included do
# TODO: Debería ser un Job?
after_create :index_posts!
has_many :indexed_posts, dependent: :destroy
MODIFIED_STATUSES = %i[added modified].freeze
DELETED_STATUSES = %i[deleted].freeze
LOCALE_FROM_PATH = /\A_/.freeze
def index_posts!
Site.transaction do
docs.each(&:index!)
update(last_indexed_commit: repository.head_commit.oid)
end
end
# Encuentra los artículos modificados entre dos commits y los
# reindexa.
def reindex_changes!
return unless reindexable?
Site.transaction do
remove_deleted_posts!
reindex_modified_posts!
update(last_indexed_commit: repository.head_commit.oid)
end
end
# No hacer nada si el repositorio no cambió o no hubo cambios
# necesarios
def reindexable?
return false if last_indexed_commit.blank?
return false if last_indexed_commit == repository.head_commit.oid
!indexable_posts.empty?
end
private
# Trae el último commit indexado desde el repositorio
#
# @return [Rugged::Commit]
def indexed_commit
@indexed_commit ||= repository.rugged.lookup(last_indexed_commit)
end
# Calcula la diferencia entre el último commit indexado y el
# actual
#
# XXX: Esto no tiene en cuenta modificaciones en la historia como
# cambio de ramas, reverts y etc, solo asume que se mueve hacia
# adelante en la misma rama o las dos ramas están relacionadas.
#
# @return [Rugged::Diff]
def diff_with_head
@diff_with_head ||= indexed_commit.diff(repository.head_commit)
end
# Obtiene todos los archivos a reindexar
#
# @return [Array<Rugged::Delta>]
def indexable_posts
@indexable_posts ||=
diff_with_head.each_delta.select do |delta|
locales.any? do |locale|
delta.old_file[:path].start_with? "_#{locale}/"
end
end
end
# Elimina los artículos eliminados o que cambiaron de ubicación
# del índice
def remove_deleted_posts!
indexable_posts.select do |delta|
DELETED_STATUSES.include? delta.status
end.each do |delta|
locale, path = locale_and_path_from(delta.old_file[:path])
indexed_posts.destroy_by(locale: locale, path: path).tap do |destroyed_posts|
next unless destroyed_posts.empty?
Rails.logger.info I18n.t('indexed_posts.deleted', site: name, path: path, records: destroyed_posts.count)
end
end
end
# Reindexa artículos que cambiaron de ubicación, se agregaron
# o fueron modificados
def reindex_modified_posts!
indexable_posts.select do |delta|
MODIFIED_STATUSES.include? delta.status
end.each do |delta|
locale, path = locale_and_path_from(delta.new_file[:path])
posts(lang: locale).find(path).index!
end
end
# Obtiene el idioma y la ruta del post a partir de la ubicación en
# el disco.
#
# Las rutas vienen en ASCII-9BIT desde Rugged, pero en realidad
# son UTF-8
#
# @return [Array<String>]
def locale_and_path_from(path)
locale, path = path.force_encoding('utf-8').split(File::SEPARATOR, 2)
[
locale.sub(LOCALE_FROM_PATH, ''),
File.basename(path, '.*')
]
end
end
end
end

View file

@ -54,7 +54,7 @@ class Site
# Incorpora los cambios en el repositorio actual
#
# @return [Rugged::Commit]
def merge(usuarie)
def merge(usuarie, message = I18n.t('sites.fetch.merge.message'))
merge = rugged.merge_commits(head_commit, remote_head_commit)
# No hacemos nada si hay conflictos, pero notificarnos
@ -69,12 +69,16 @@ class Site
.create(rugged, update_ref: 'HEAD',
parents: [head_commit, remote_head_commit],
tree: merge.write_tree(rugged),
message: I18n.t('sites.fetch.merge.message'),
message: message,
author: author(usuarie), committer: committer)
# Forzamos el checkout para mover el HEAD al último commit y
# escribir los cambios
rugged.checkout 'HEAD', strategy: :force
git_sh("git", "lfs", "fetch", "origin", default_branch)
# reemplaza los pointers por los archivos correspondientes
git_sh("git", "lfs", "checkout")
commit
end
@ -165,6 +169,12 @@ class Site
git_sh('git', 'lfs', 'push', remote.name, default_branch)
end
# Hace limpieza de LFS
def lfs_cleanup
git_sh("git", "lfs", "prune")
git_sh("git", "lfs", "dedup")
end
private
# @deprecated

View file

@ -16,7 +16,7 @@ class Site
#
# @return [nil]
def generate_private_key_pem!
self.private_key_pem ||= DistributedPress::V1::Social::Client.new(public_key_url: nil, key_size: 2048).private_key.export
self.private_key_pem ||= ::DistributedPress::V1::Social::Client.new(public_key_url: nil, key_size: 2048).private_key.export
end
end
end

View file

@ -10,6 +10,7 @@ class Usuarie < ApplicationRecord
validates_uniqueness_of :email
validates_with EmailAddress::ActiveRecordValidator, field: :email
validate :locale_available!
before_create :lang_from_locale!
before_update :remove_confirmation_invitation_inconsistencies!
@ -78,4 +79,15 @@ class Usuarie < ApplicationRecord
self.invitation_accepted_at ||= Time.now.utc
end
end
# Muestra un error si el idioma no está disponible al cambiar el
# idioma de la cuenta.
#
# @return [nil]
def locale_available!
return if I18n.locale_available? self.lang
errors.add(:lang, I18n.t('activerecord.errors.models.usuarie.attributes.lang.not_available'))
nil
end
end

View file

@ -0,0 +1,66 @@
# frozen_string_literal: true
# Política de acceso a artículos
class IndexedPostPolicy
attr_reader :indexed_post, :usuarie, :site
def initialize(usuarie, indexed_post)
@usuarie = usuarie
@indexed_post = indexed_post
@site = indexed_post.site
end
def index?
true
end
# Les invitades solo pueden ver sus propios posts
def show?
site.usuarie?(usuarie) || site.indexed_posts.by_usuarie(usuarie.id).find_by_post_id(indexed_post.post_id).present?
end
def preview?
show?
end
def new?
create?
end
def create?
true
end
def edit?
update?
end
# Les invitades solo pueden modificar sus propios artículos
def update?
show?
end
# Solo las usuarias pueden eliminar artículos. Les invitades pueden
# borrar sus propios artículos
def destroy?
update?
end
# Las usuarias pueden ver todos los posts
#
# Les invitades solo pueden ver sus propios posts
class Scope
attr_reader :usuarie, :scope
def initialize(usuarie, scope)
@usuarie = usuarie
@scope = scope
end
def resolve
return scope if scope&.first&.site&.usuarie? usuarie
scope.by_usuarie(usuarie.id)
end
end
end

View file

@ -23,12 +23,15 @@ class CleanupService
#
# @return [nil]
def cleanup_older_sites!
Site.where('updated_at < ?', before).find_each do |site|
Site.where('updated_at < ?', before).order(updated_at: :desc).find_each do |site|
next unless File.directory? site.path
Rails.logger.info "Limpiando dependencias, archivos temporales y repositorio git de #{site.name}"
site.deploys.find_each(&:cleanup!)
site.repository.gc
site.repository.lfs_cleanup
site.touch
end
end
@ -37,10 +40,13 @@ class CleanupService
#
# @return [nil]
def cleanup_newer_sites!
Site.where('updated_at >= ?', before).find_each do |site|
Site.where('updated_at >= ?', before).order(updated_at: :desc).find_each do |site|
next unless File.directory? site.path
Rails.logger.info "Limpiando repositorio git de #{site.name}"
site.repository.gc
site.repository.lfs_cleanup
site.touch
end
end

View file

@ -33,6 +33,7 @@ SiteService = Struct.new(:site, :usuarie, :params, keyword_init: true) do
add_licencias &&
add_code_of_conduct &&
add_privacy_policy &&
site.index_posts! &&
deploy
end

View file

@ -25,4 +25,4 @@
class: 'form-control'
.form-group
= f.submit t('.submit'), class: 'btn btn-lg btn-block'
= f.submit t('.submit'), class: 'btn btn-secondary btn-lg btn-block'

View file

@ -30,5 +30,5 @@
placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.resend_confirmation_instructions'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -32,4 +32,4 @@
placeholder: t('activerecord.attributes.usuarie.password')
.actions
= f.submit t('devise.invitations.edit.submit_button'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'

View file

@ -16,4 +16,4 @@
= f.text_field field, class: 'form-control'
.actions
= f.submit t('devise.invitations.new.submit_button'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'

View file

@ -39,6 +39,6 @@
.actions
= f.submit t('.change_my_password'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -20,5 +20,5 @@
placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.send_me_reset_password_instructions'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -55,7 +55,7 @@
= t('.we_need_your_current_password_to_confirm_your_changes')
.actions
= f.submit t('.update'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
%hr/
.sr-only
@ -63,4 +63,4 @@
= button_to t('.cancel_my_account'),
registration_path(resource_name),
data: { confirm: t('.are_you_sure') },
method: :delete, class: 'btn btn-block'
method: :delete, class: 'btn btn-secondary btn-block'

View file

@ -56,6 +56,6 @@
.actions
= f.submit t('.sign_up'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -35,5 +35,5 @@
remember_for: distance_of_time_in_words(Usuarie.remember_for))
.actions
= f.submit t('.sign_in'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -4,12 +4,12 @@
- if controller_name != 'sessions'
= link_to t('.sign_in'), new_session_path(resource_name, params: locale),
class: 'btn btn-lg btn-block btn-success'
class: 'btn btn-lg btn-block btn-secondary'
%br/
- if devise_mapping.registerable? && controller_name != 'registrations'
= link_to t('.sign_up'), new_registration_path(resource_name, params: locale),
class: 'btn btn-lg btn-block btn-success'
class: 'btn btn-lg btn-block btn-secondary'
%br/
- if devise_mapping.recoverable?

View file

@ -20,5 +20,5 @@
placeholder: t('activerecord.attributes.usuarie.email')
.actions
= f.submit t('.resend_unlock_instructions'),
class: 'btn btn-lg btn-block'
class: 'btn btn-secondary btn-lg btn-block'
= render 'devise/shared/links'

View file

@ -11,7 +11,7 @@
= select_tag 'to',
options_for_select(@options, @lang_to),
class: 'form-control'
= submit_tag t('i18n.translate'), class: 'btn', name: nil
= submit_tag t('i18n.translate'), class: 'btn btn-secondary', name: nil
- else
= t('i18n.translating.from')
= select_tag 'from',
@ -21,7 +21,7 @@
= select_tag 'to',
options_for_select(@options, @lang_to),
class: 'form-control'
= submit_tag t('i18n.change'), class: 'btn', name: nil
= submit_tag t('i18n.change'), class: 'btn btn-secondary', name: nil
= render 'layouts/help', help: t('help.i18n.index')
@ -33,16 +33,16 @@
= hidden_field 'i18n', 'lang_to', value: @lang_to
.form-group
.dropdown.inline
%button.btn.dropdown-toggle{type: 'button',
%button.btn.btn-secondary.dropdown-toggle{type: 'button',
data: { toggle: 'dropdown' },
aria: { haspopup: 'true', expanded: 'false' }}
= t('i18n.jump')
.dropdown-menu{aria: { labelledby: t('i18n.jump') }}
- @site.data.dig(@lang_from).each_pair do |section, content|
%a.dropdown-item{href: "##{section}"}= t("help.i18n.#{section}")
= submit_tag t('i18n.save'), class: 'btn'
= submit_tag t('i18n.save'), class: 'btn btn-secondary'
= render 'i18n/recursive', data: @site.data.dig(@lang_from), superkeys: []
.form-group
= submit_tag t('i18n.save'), class: 'btn'
= submit_tag t('i18n.save'), class: 'btn btn-secondary'

View file

@ -17,15 +17,15 @@
- if @site&.tienda?
%li.nav-item
= link_to t('.tienda'), @site.tienda_url,
role: 'button', class: 'btn'
role: 'button', class: 'btn btn-secondary'
%li.nav-item
= link_to t('.contact_us'), t('.contact_us_href'),
class: 'btn', rel: 'me', target: '_blank'
class: 'btn btn-secondary', rel: 'me', target: '_blank'
%li.nav-item
= link_to t('.logout'), main_app.destroy_usuarie_session_path,
method: :delete, role: 'button', class: 'btn'
method: :delete, role: 'button', class: 'btn btn-secondary'
- else
- params.permit!
- I18n.available_locales.each do |locale|

View file

@ -1,2 +1,2 @@
= link_to text, link, class: 'btn',
= link_to text, link, class: 'btn btn-secondary',
data: { toggle: 'tooltip' }, 'aria-role': 'button', title: tooltip

View file

@ -14,6 +14,7 @@
%script{ type: 'text/javascript', src: '/env.js' }
= csrf_meta_tags
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= stylesheet_link_tag 'dark', rel: 'alternate stylesheet', media: 'all', 'data-turbolinks-track': 'reload', title: t('dark')
= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload'
= stylesheet_pack_tag 'application', 'data-turbolinks-track': 'reload'
= favicon_link_tag 'sutty_cuadrada.png', rel: 'apple-touch-icon', type: 'image/png'

View file

@ -1,7 +1,7 @@
- invalid_help = site.config.fetch('invalid_help', t('.invalid_help'))
- sending_help = site.config.fetch('sending_help', t('.sending_help'))
.form-group
= submit_tag t('.save'), class: 'btn submit-post'
= submit_tag t('.save'), class: 'btn btn-secondary submit-post'
= render 'bootstrap/alert', class: 'invalid-help d-none' do
= invalid_help
= render 'bootstrap/alert', class: 'sending-help d-none' do

View file

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

View file

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

View file

@ -3,7 +3,7 @@
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata
.editor{ id: attribute, data: { editor: '' } }
.old.editor{ id: attribute, data: { editor: '' } }
-# Esto es para luego decirle al navegador que se olvide estas cosas.
= hidden_field_tag 'storage_keys[]', "#{request.original_url}##{attribute}", data: { target: 'storage-key' }
= render 'bootstrap/alert' do
@ -20,82 +20,82 @@
TODO: Eliminar todo el espacio en blanco para minificar HTML
.editor-toolbar{ style: 'z-index: 1' }
.editor-primary-toolbar.scrollbar-black
%button.btn{ type: 'button', title: t('editor.multimedia'), data: { editor_button: 'multimedia' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.multimedia'), data: { editor_button: 'multimedia' } }>
%i.fa.fa-fw.fa-upload>
%span.sr-only>= t('editor.multimedia')
%button.btn{ type: 'button', title: t('editor.bold'), data: { editor_button: 'mark-bold' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.bold'), data: { editor_button: 'mark-bold' } }>
%i.fa.fa-fw.fa-bold>
%span.sr-only>= t('editor.bold')
%button.btn{ type: 'button', title: t('editor.italic'), data: { editor_button: 'mark-italic' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.italic'), data: { editor_button: 'mark-italic' } }>
%i.fa.fa-fw.fa-italic>
%span.sr-only>= t('editor.italic')
%button.btn{ type: 'button', title: t('editor.mark'), data: { editor_button: 'mark-mark' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.mark'), data: { editor_button: 'mark-mark' } }>
%i.fa.fa-fw.fa-tint>
%span.sr-only>= t('editor.mark')
%button.btn{ type: 'button', title: t('editor.link'), data: { editor_button: 'mark-link' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.link'), data: { editor_button: 'mark-link' } }>
%i.fa.fa-fw.fa-link>
%span.sr-only>= t('editor.link')
%button.btn{ type: 'button', title: t('editor.deleted'), data: { editor_button: 'mark-deleted' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.deleted'), data: { editor_button: 'mark-deleted' } }>
%i.fa.fa-fw.fa-strikethrough>
%span.sr-only>= t('editor.deleted')
%button.btn{ type: 'button', title: t('editor.underline'), data: { editor_button: 'mark-underline' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.underline'), data: { editor_button: 'mark-underline' } }>
%i.fa.fa-fw.fa-underline>
%span.sr-only>= t('editor.underline')
%button.btn{ type: 'button', title: t('editor.super'), data: { editor_button: 'mark-super' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.super'), data: { editor_button: 'mark-super' } }>
%i.fa.fa-fw.fa-superscript>
%span.sr-only>= t('editor.super')
%button.btn{ type: 'button', title: t('editor.sub'), data: { editor_button: 'mark-sub' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.sub'), data: { editor_button: 'mark-sub' } }>
%i.fa.fa-fw.fa-subscript>
%span.sr-only>= t('editor.sub')
%button.btn{ type: 'button', title: t('editor.small'), data: { editor_button: 'mark-small' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.small'), data: { editor_button: 'mark-small' } }>
%i.fa.fa-fw.fa-subscript>
%span.sr-only>= t('editor.small')
%button.btn.mr-0{ type: 'button', title: t('editor.h1'), data: { editor_button: 'block-h1' } }>
%button.btn.btn-secondary.mr-0{ type: 'button', title: t('editor.h1'), data: { editor_button: 'block-h1' } }>
%i.fa.fa-fw.fa-heading>
1
%span.sr-only>= t('editor.h1')
%details.d-inline>
%summary.d-inline>
%span.btn.ml-0{ role: 'button', title: t('editor.more') }>
%span.btn.btn-secondary.ml-0{ role: 'button', title: t('editor.more') }>
%i.fa.fa-caret-right>
%span.sr-only= t('editor.more')
.d-inline>
%button.btn{ type: 'button', title: t('editor.h2'), data: { editor_button: 'block-h2' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h2'), data: { editor_button: 'block-h2' } }>
%i.fa.fa-fw.fa-heading>
2
%span.sr-only>= t('editor.h2')
%button.btn{ type: 'button', title: t('editor.h3'), data: { editor_button: 'block-h3' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h3'), data: { editor_button: 'block-h3' } }>
%i.fa.fa-fw.fa-heading>
3
%span.sr-only>= t('editor.h3')
%button.btn{ type: 'button', title: t('editor.h4'), data: { editor_button: 'block-h4' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h4'), data: { editor_button: 'block-h4' } }>
%i.fa.fa-fw.fa-heading>
4
%span.sr-only>= t('editor.h4')
%button.btn{ type: 'button', title: t('editor.h5'), data: { editor_button: 'block-h5' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h5'), data: { editor_button: 'block-h5' } }>
%i.fa.fa-fw.fa-heading>
5
%span.sr-only>= t('editor.h5')
%button.btn{ type: 'button', title: t('editor.h6'), data: { editor_button: 'block-h6' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.h6'), data: { editor_button: 'block-h6' } }>
%i.fa.fa-fw.fa-heading>
6
%span.sr-only>= t('editor.h6')
%button.btn{ type: 'button', title: t('editor.ul'), data: { editor_button: 'block-unordered_list' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.ul'), data: { editor_button: 'block-unordered_list' } }>
%i.fa.fa-fw.fa-list-ul>
%span.sr-only>= t('editor.ul')
%button.btn{ type: 'button', title: t('editor.ol'), data: { editor_button: 'block-ordered_list' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.ol'), data: { editor_button: 'block-ordered_list' } }>
%i.fa.fa-fw.fa-list-ol>
%span.sr-only>= t('editor.ol')
%button.btn{ type: 'button', title: t('editor.left'), data: { editor_button: 'parentBlock-left' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.left'), data: { editor_button: 'parentBlock-left' } }>
%i.fa.fa-fw.fa-align-left>
%span.sr-only>= t('editor.left')
%button.btn{ type: 'button', title: t('editor.center'), data: { editor_button: 'parentBlock-center' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.center'), data: { editor_button: 'parentBlock-center' } }>
%i.fa.fa-fw.fa-align-center>
%span.sr-only>= t('editor.center')
%button.btn{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.right'), data: { editor_button: 'parentBlock-right' } }>
%i.fa.fa-fw.fa-align-right>
%span.sr-only>= t('editor.right')
%button.btn{ type: 'button', title: t('editor.blockquote'), data: { editor_button: 'block-blockquote' } }>
%button.btn.btn-secondary{ type: 'button', title: t('editor.blockquote'), data: { editor_button: 'block-blockquote' } }>
%i.fa.fa-fw.fa-quote-left>
%span.sr-only>= t('editor.blockquote')
@ -116,8 +116,8 @@
%label{ for: 'multimedia-alt' }= t('editor.description')
%input.form-control{ type: 'text', id: 'multimedia-alt', name: 'multimedia-alt' }/
.form-group
%button.btn{ type: 'button', id: 'multimedia-file-upload', name: 'multimedia-file-upload' }= t('editor.multimedia-upload')
%button.btn{ type: 'button', id: 'multimedia-remove', name: 'multimedia-remove' }= t('editor.multimedia-remove')
%button.btn.btn-secondary{ type: 'button', id: 'multimedia-file-upload', name: 'multimedia-file-upload' }= t('editor.multimedia-upload')
%button.btn.btn-secondary{ type: 'button', id: 'multimedia-remove', name: 'multimedia-remove' }= t('editor.multimedia-remove')
.form-group{ data: { editor_auxiliary: 'link' } }
%label{ for: 'link-url' }= t('editor.url')
@ -127,4 +127,4 @@
%p= t('editor.word')
.editor-content.form-control.h-auto.mt-1{ contenteditable: 'true' }
= metadata.value.html_safe
= metadata.to_s.html_safe

View file

@ -0,0 +1,9 @@
.form-group
= label_tag "#{base}_#{attribute}", post_label_t(attribute, post: post)
= render 'posts/attribute_feedback',
post: post, attribute: attribute, metadata: metadata
.new-editor.content{ id: attribute }
= text_area_tag "#{base}[#{attribute}]", metadata.to_s.html_safe,
dir: dir, lang: locale,
**field_options(attribute, metadata), class: 'd-none'

View file

@ -0,0 +1,6 @@
-# Editor de contenido
= render 'posts/attributes/new_content',
base: 'post', post: post, attribute: attribute,
metadata: metadata, site: site,
dir: dir, locale: locale,
autofocus: (post.attributes.first == attribute)

View file

@ -1,3 +1,6 @@
.row.justify-content-center
.col-md-8
= render 'posts/form', site: @site, post: @post
= render 'layouts/details', summary: "Post" do
= render 'posts/form', site: @site, post: @post
= render 'layouts/details', summary: t('.moderation_queue') do
= render 'posts/moderation_queue', site: @site, post: @post, moderation_queue: @moderation_queue

View file

@ -15,13 +15,13 @@
= render 'schemas/row', site: @site, schema: schema, filter: @filter_params
- if policy(@site_stat).index?
= link_to t('stats.index.title'), site_stats_path(@site), class: 'btn'
= link_to t('stats.index.title'), site_stats_path(@site), class: 'btn btn-secondary'
- if policy(@site).edit?
= link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn'
= link_to t('sites.edit.btn', site: @site.title), edit_site_path(@site), class: 'btn btn-secondary'
- if policy(@site).private?
= link_to t('sites.private'), '../private/' + @site.name, class: 'btn', target: '_blank', rel: 'noopener'
= link_to t('sites.private'), '../private/' + @site.name, class: 'btn btn-secondary', target: '_blank', rel: 'noopener'
- if policy(SiteUsuarie.new(@site, current_usuarie)).index?
= render 'layouts/btn_with_tooltip',
@ -33,9 +33,9 @@
- if @site.design.credits
= render 'bootstrap/alert' do
= sanitize_markdown @site.design.credits
= link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn'
= link_to t('sites.donations.text'), t('sites.donations.url'), class: 'btn btn-secondary'
- if @site.design.designer_url
= link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn'
= link_to t('sites.designer_url'), @site.design.designer_url, class: 'btn btn-secondary'
%section.col
.d-flex.justify-content-between.align-items-center.pl-2-plus.pr-2-plus.mb-2
@ -75,19 +75,19 @@
%th.border-0{ colspan: '4' }
.d-flex.flex-row.justify-content-between
%div
= submit_tag t('posts.reorder.submit'), class: 'btn'
%button.btn{ data: { action: 'reorder#unselect' } }
= submit_tag t('posts.reorder.submit'), class: 'btn btn-secondary'
%button.btn.btn-secondary{ data: { action: 'reorder#unselect' } }
= t('posts.reorder.unselect')
%span.badge{ data: { target: 'reorder.counter' } } 0
%button.btn{ data: { action: 'reorder#up' } }= t('posts.reorder.up')
%button.btn{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
%button.btn{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
%button.btn.btn-secondary{ data: { action: 'reorder#up' } }= t('posts.reorder.up')
%button.btn.btn-secondary{ data: { action: 'reorder#down' } }= t('posts.reorder.down')
%button.btn.btn-secondary{ data: { action: 'reorder#top' } }= t('posts.reorder.top')
%button.btn.btn-secondary{ data: { action: 'reorder#bottom' } }= t('posts.reorder.bottom')
- if @site.pagination
%div
= link_to_prev_page @posts, t('posts.prev'), class: 'btn'
= link_to_next_page @posts, t('posts.next'), class: 'btn'
= link_to_prev_page @posts, t('posts.prev'), class: 'btn btn-secondary'
= link_to_next_page @posts, t('posts.next'), class: 'btn btn-secondary'
%tbody
- dir = @site.data.dig(params[:locale], 'dir')
- size = @posts.size
@ -126,9 +126,9 @@
= post.order
%td.text-nowrap
- if @usuarie || policy(post).edit?
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-block'
= link_to t('posts.edit'), edit_site_post_path(@site, post.path), class: 'btn btn-secondary btn-block'
- if @usuarie || policy(post).destroy?
= link_to t('posts.destroy'), site_post_path(@site, post.path), class: 'btn btn-block', method: :delete, data: { confirm: t('posts.confirm_destroy') }
= link_to t('posts.destroy'), site_post_path(@site, post.path), class: 'btn btn-secondary btn-block', method: :delete, data: { confirm: t('posts.confirm_destroy') }
#footnotes{ hidden: true }
- @filter_params.each do |param, value|

View file

@ -4,7 +4,7 @@
%article.content.table-responsive-md
= link_to t('posts.edit'),
edit_site_post_path(@site, @post.id),
class: 'btn btn-block'
class: 'btn btn-secondary btn-block'
%table.table.table-condensed
%thead
@ -30,5 +30,5 @@
- next if metadata.front_matter?
- cache [metadata, I18n.locale] do
%section.editor{ id: attr, dir: dir }
= @post.public_send(attr).value.html_safe
%section.content.pb-3{ id: attr, dir: dir }
= @post.public_send(attr).to_s.html_safe

View file

@ -3,7 +3,7 @@
method: :post,
class: 'form-inline inline' do
= submit_tag site.enqueued? ? t('sites.enqueued') : t('sites.enqueue'),
class: "btn no-border-radius #{local_assigns[:class]}",
class: "btn btn-secondary #{local_assigns[:class]}",
title: site.enqueued? ? t('help.sites.enqueued') : t('help.sites.enqueue'),
data: { disable_with: t('sites.enqueued') },
disabled: site.enqueued?

View file

@ -46,36 +46,37 @@
.invalid-feedback= site.errors.messages[:description].join(', ')
%hr/
.form-group#design_id
%h2= t('.design.title')
%p.lead= t('.help.design')
- if invalid? site, :design_id
= render 'bootstrap/alert' do
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
layouts: site.incompatible_layouts.to_sentence)
.row.row-cols-1.row-cols-md-2.designs
-# Demasiado complejo para un f.collection_radio_buttons
- Design.all.order(priority: :desc).each do |design|
.design.col.d-flex.flex-column
.custom-control.custom-radio
= f.radio_button :design_id, design.id,
checked: design.id == site.design_id,
disabled: design.disabled,
required: true, class: 'custom-control-input'
= f.label "design_id_#{design.id}", design.name,
class: 'custom-control-label'
.flex-fill
= sanitize_markdown design.description,
tags: %w[p a strong em]
- unless site.persisted?
.form-group#design_id
%h2= t('.design.title')
%p.lead= t('.help.design')
- if invalid? site, :design_id
= render 'bootstrap/alert' do
= t('activerecord.errors.models.site.attributes.design_id.layout_incompatible.help',
layouts: site.incompatible_layouts.to_sentence)
.row.row-cols-1.row-cols-md-2.designs
-# Demasiado complejo para un f.collection_radio_buttons
- Design.all.order(priority: :desc).each do |design|
.design.col.d-flex.flex-column
.custom-control.custom-radio
= f.radio_button :design_id, design.id,
checked: design.id == site.design_id,
disabled: design.disabled,
required: true, class: 'custom-control-input'
= f.label "design_id_#{design.id}", design.name,
class: 'custom-control-label'
.flex-fill
= sanitize_markdown design.description,
tags: %w[p a strong em]
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
- if design.url
= link_to t('.design.url'), design.url,
target: '_blank', class: 'btn'
- if design.license
= link_to t('.design.license'), design.license,
target: '_blank', class: 'btn'
%hr/
.btn-group{ role: 'group', 'aria-label': t('.design.actions') }
- if design.url
= link_to t('.design.url'), design.url,
target: '_blank', class: 'btn btn-secondary'
- if design.license
= link_to t('.design.license'), design.license,
target: '_blank', class: 'btn btn-secondary'
%hr/
.form-group.licenses#license_id
%h2= t('.licencia.title')
@ -98,7 +99,7 @@
tags: %w[p a strong em ul ol li h1 h2 h3 h4 h5 h6]
- unless licencia.custom?
= link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn', rel: 'noopener'
= link_to t('.licencia.url'), licencia.url, target: '_blank', class: 'btn btn-secondary', rel: 'noopener'
%hr/
@ -162,4 +163,4 @@
deploy: deploy, site: site
.form-group
= f.submit submit, class: 'btn btn-lg btn-block'
= f.submit submit, class: 'btn btn-secondary btn-lg btn-block'

View file

@ -27,4 +27,4 @@
.row.justify-content-center
.col-md-8
= link_to t('.merge.request'), site_pull_path(@site),
method: 'post', class: 'btn btn-lg'
method: 'post', class: 'btn btn-secondary btn-lg'

View file

@ -4,7 +4,7 @@
%p.lead= t('.help')
- if policy(Site).new?
= link_to t('sites.new.title'), new_site_path,
class: 'btn'
class: 'btn btn-secondary'
%section.col
- if @sites.empty?
@ -29,18 +29,18 @@
= site.title
%p.lead= site.description
%br
= link_to t('.visit'), site.url, class: 'btn'
= link_to t('.visit'), site.url, class: 'btn btn-secondary'
- if rol.temporal
= button_to t('sites.invitations.accept'),
site_usuaries_accept_invitation_path(site),
method: :patch,
title: t('help.sites.invitations.accept'),
class: 'btn'
class: 'btn btn-secondary'
= button_to t('sites.invitations.reject'),
site_usuaries_reject_invitation_path(site),
method: :patch,
title: t('help.sites.invitations.reject'),
class: 'btn'
class: 'btn btn-secondary'
- else
- if policy(site).show?
= render 'layouts/btn_with_tooltip',
@ -54,10 +54,4 @@
text: t('usuaries.index.title'),
type: 'info',
link: site_usuaries_path(site)
- if policy(site).pull? && site.repository.needs_pull?
= render 'layouts/btn_with_tooltip',
tooltip: t('help.sites.pull'),
text: t('.pull'),
type: 'info',
link: site_pull_path(site)
= render 'sites/build', site: site

View file

@ -11,11 +11,11 @@
%form.mb-5.form-inline{ method: 'get' }
- Stat::INTERVALS.each do |interval|
= link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls], period_start: params[:period_start].to_date.try(:"beginning_of_#{interval}").to_date, period_end: params[:period_end]), class: "mb-0 btn #{'btn-primary active' if @interval == interval}"
= link_to t(".#{interval}"), site_stats_path(interval: interval, urls: params[:urls], period_start: params[:period_start].to_date.try(:"beginning_of_#{interval}").to_date, period_end: params[:period_end]), class: "mb-0 btn #{@interval == interval ? 'btn-primary active' : 'btn-secondary' }"
%input.form-control{ type: 'date', name: :period_start, value: params[:period_start] }
%input.form-control{ type: 'date', name: :period_end, value: params[:period_end] }
%button.btn.mb-0{ type: 'submit' }= t('.filter')
%button.btn.btn-secondary.mb-0{ type: 'submit' }= t('.filter')
.mb-5
%h2= t('.host.title', count: @hostnames.size)
@ -34,7 +34,7 @@
%textarea#urls.form-control{ name: 'urls', autocomplete: 'on', required: true, rows: @normalized_urls.size + 1, aria_describedby: 'help-urls' }= @normalized_urls.join("\n")
%small#help-urls.feedback.form-text.text-muted= t('.urls.help')
.form-group
%button.btn{ type: 'submit' }= t('.urls.submit')
%button.btn.btn-secondary{ type: 'submit' }= t('.urls.submit')
- if @normalized_urls.present?
= line_chart site_stats_uris_path(urls: @normalized_urls, **@chart_params), **@chart_options

View file

@ -9,13 +9,13 @@
- if @policy.invite?
= link_to t('.invite'),
site_usuaries_invite_path(@site, invite_as: u.to_s),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip' },
title: t('.help.invite', invite_as: u.to_s)
- if policy(Collaboration.new(@site)).collaborate?
= link_to t('.public_invite'),
site_collaborate_path(@site),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip' },
title: t('.help.public_invite')
%p.lead= t(".help.#{u}")
@ -38,7 +38,7 @@
- if @policy.demote? && @site.usuarie?(cuenta)
= link_to t('.demote.text'),
site_usuarie_demote_path(@site, cuenta),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip',
confirm: t('.demote.confirm') },
title: t('.help.demote'),
@ -46,7 +46,7 @@
- if @policy.promote? && @site.invitade?(cuenta)
= link_to t('.promote.text'),
site_usuarie_promote_path(@site, cuenta),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip',
confirm: t('.promote.confirm') },
title: t('.help.promote'),
@ -54,7 +54,7 @@
- if @policy.destroy?
= link_to t('.destroy.text'),
site_usuarie_path(@site, cuenta),
class: 'btn',
class: 'btn btn-secondary',
data: { toggle: 'tooltip',
confirm: t('.destroy.confirm') },
title: t('.help.destroy'),

View file

@ -13,4 +13,4 @@
invite_as: invite_as)
= f.text_area :invitaciones, class: 'form-control'
.form-group
= f.submit t('.submit'), class: 'btn'
= f.submit t('.submit'), class: 'btn btn-secondary'

8
bin/modified_files Executable file
View file

@ -0,0 +1,8 @@
#!/bin/sh
set -e
test -n "${CI_MERGE_REQUEST_DIFF_BASE_SHA}"
git diff --name-status ${CI_MERGE_REQUEST_DIFF_BASE_SHA} \
| grep -v "^D" \
| cut -f 2

4
bin/with_extension Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
grep "\.${1}$"
exit 0

View file

@ -21,6 +21,17 @@ require 'rails/test_unit/railtie'
# you've limited to :test, :development, or :production.
Bundler.require(*Rails.groups)
if %w[development test].include? ENV['RAILS_ENV']
# https://github.com/bkeepers/dotenv/pull/453
Dotenv::Railtie.class_eval do
def overload
Dotenv.overload(*dotenv_files.reverse)
end
end
Dotenv::Railtie.overload
end
module Sutty
# Sutty!
class Application < Rails::Application
@ -45,6 +56,8 @@ module Sutty
config.active_storage.queues.purge = :default
config.active_job.queue_adapter = :que
config.active_record.schema_format = :sql
config.to_prepare do
# Load application's model / class decorators
Dir.glob(File.join(File.dirname(__FILE__), '..', 'app', '**', '*_decorator.rb')).sort.each do |c|

252
config/brakeman.ignore Normal file
View file

@ -0,0 +1,252 @@
{
"ignored_warnings": [
{
"warning_type": "Redirect",
"warning_code": 18,
"fingerprint": "0ae5c3990d49dfbfd4fd61874451f7a576d5056aca913068adf58c314625f810",
"check_name": "Redirect",
"message": "Possible unprotected redirect",
"file": "app/controllers/api/v1/posts_controller.rb",
"line": 20,
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
"code": "redirect_to((params[:redirect_to] or origin.to_s))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::PostsController",
"method": "create"
},
"user_input": "params[:redirect_to]",
"confidence": "High",
"cwe_id": [
601
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14957"
},
{
"warning_type": "Denial of Service",
"warning_code": 76,
"fingerprint": "1947d1a2ae6e4bf718d0cc563e660efca96897165e9a8dd18186c1d7abe6ddf6",
"check_name": "RegexDoS",
"message": "Model attribute used in regular expression",
"file": "app/controllers/api/v1/base_controller.rb",
"line": 20,
"link": "https://brakemanscanner.org/docs/warning_types/denial_of_service/",
"code": "/\\.#{Site.domain}\\z/",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::BaseController",
"method": "site_id"
},
"user_input": "Site.domain",
"confidence": "Medium",
"cwe_id": [
20,
185
],
"note": "No es un atributo, es una variable de entorno"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 4,
"fingerprint": "28d98d08a15c4b3ad94a2cfa20a12573de12d99f1a30b3ca51074ee1f1886592",
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in `link_to` href",
"file": "app/views/layouts/_breadcrumb.haml",
"line": 19,
"link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(t(\".tienda\"), Site.find(params[:site_id]).tienda_url, :role => \"button\", :class => \"btn\")",
"render_path": [
{
"type": "controller",
"class": "Api::V1::NoticesController",
"method": "site",
"line": 31,
"file": "app/controllers/api/v1/notices_controller.rb",
"rendered": {
"name": "layouts/application",
"file": "app/views/layouts/application.html.haml"
}
},
{
"type": "template",
"name": "layouts/application",
"line": 25,
"file": "app/views/layouts/application.html.haml",
"rendered": {
"name": "layouts/_breadcrumb",
"file": "app/views/layouts/_breadcrumb.haml"
}
}
],
"location": {
"type": "template",
"template": "layouts/_breadcrumb"
},
"user_input": "Site.find(params[:site_id]).tienda_url",
"confidence": "Weak",
"cwe_id": [
79
],
"note": ""
},
{
"warning_type": "Redirect",
"warning_code": 18,
"fingerprint": "5034e51aaa1bac06d15fdde5956edffbfd65f94f5620a409526bbea896dc7b5f",
"check_name": "Redirect",
"message": "Possible unprotected redirect",
"file": "app/controllers/api/v1/contact_controller.rb",
"line": 26,
"link": "https://brakemanscanner.org/docs/warning_types/redirect/",
"code": "redirect_to((params[:redirect] or origin.to_s))",
"render_path": null,
"location": {
"type": "method",
"class": "Api::V1::ContactController",
"method": "receive"
},
"user_input": "params[:redirect]",
"confidence": "High",
"cwe_id": [
601
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14957"
},
{
"warning_type": "Mass Assignment",
"warning_code": 70,
"fingerprint": "50582f39f8dfa900d3f2b5b9908b1592f8b8bd9e2d0b9d1cc05d77e5ede2d94e",
"check_name": "MassAssignment",
"message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys",
"file": "app/views/layouts/_link_rel_alternate.haml",
"line": 2,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit!",
"render_path": [
{
"type": "controller",
"class": "Api::V1::BaseController",
"method": "site_id",
"line": 20,
"file": "app/controllers/api/v1/base_controller.rb",
"rendered": {
"name": "layouts/application",
"file": "app/views/layouts/application.html.haml"
}
},
{
"type": "template",
"name": "layouts/application",
"line": 21,
"file": "app/views/layouts/application.html.haml",
"rendered": {
"name": "layouts/_link_rel_alternate",
"file": "app/views/layouts/_link_rel_alternate.haml"
}
}
],
"location": {
"type": "template",
"template": "layouts/_link_rel_alternate"
},
"user_input": null,
"confidence": "Medium",
"cwe_id": [
915
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14958"
},
{
"warning_type": "Mass Assignment",
"warning_code": 70,
"fingerprint": "b8e0aa898288bebb614ccc1340d169caa196d315c6ac2e4744081cc892c2ae97",
"check_name": "MassAssignment",
"message": "Specify exact keys allowed for mass assignment instead of using `permit!` which allows any keys",
"file": "app/views/layouts/_breadcrumb.haml",
"line": 30,
"link": "https://brakemanscanner.org/docs/warning_types/mass_assignment/",
"code": "params.permit!",
"render_path": [
{
"type": "controller",
"class": "Api::V1::BaseController",
"method": "site_id",
"line": 20,
"file": "app/controllers/api/v1/base_controller.rb",
"rendered": {
"name": "layouts/application",
"file": "app/views/layouts/application.html.haml"
}
},
{
"type": "template",
"name": "layouts/application",
"line": 25,
"file": "app/views/layouts/application.html.haml",
"rendered": {
"name": "layouts/_breadcrumb",
"file": "app/views/layouts/_breadcrumb.haml"
}
}
],
"location": {
"type": "template",
"template": "layouts/_breadcrumb"
},
"user_input": null,
"confidence": "Medium",
"cwe_id": [
915
],
"note": "https://0xacab.org/sutty/sutty/-/issues/14958"
},
{
"warning_type": "Cross-Site Scripting",
"warning_code": 4,
"fingerprint": "c051421c7cf4c2706b8e27bfd2f3b0661ec6a6df873da322a6b634b59e80351b",
"check_name": "LinkToHref",
"message": "Potentially unsafe model attribute in `link_to` href",
"file": "app/views/sites/_form.haml",
"line": 74,
"link": "https://brakemanscanner.org/docs/warning_types/link_to_href",
"code": "link_to(t(\".design.url\"), (Unresolved Model).new.url, :target => \"_blank\", :class => \"btn\")",
"render_path": [
{
"type": "controller",
"class": "SitesController",
"method": "new",
"line": 31,
"file": "app/controllers/sites_controller.rb",
"rendered": {
"name": "sites/new",
"file": "app/views/sites/new.haml"
}
},
{
"type": "template",
"name": "sites/new",
"line": 6,
"file": "app/views/sites/new.haml",
"rendered": {
"name": "sites/_form",
"file": "app/views/sites/_form.haml"
}
}
],
"location": {
"type": "template",
"template": "sites/_form"
},
"user_input": "(Unresolved Model).new.url",
"confidence": "Weak",
"cwe_id": [
79
],
"note": ""
}
],
"updated": "2024-01-11 18:12:14 -0300",
"brakeman_version": "5.4.1"
}

View file

@ -142,7 +142,7 @@ Rails.application.configure do
}
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', "noreply@sutty.nl") }
config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: (['DeployJob::DeployAlreadyRunningException'] + ExceptionNotifier.ignored_exceptions)
config.middleware.use ExceptionNotification::Rack, gitlab: {}, ignore_exceptions: ['DeployJob::DeployAlreadyRunningException']
Rails.application.routes.default_url_options[:host] = "panel.#{ENV.fetch('SUTTY', 'sutty.nl')}"
Rails.application.routes.default_url_options[:protocol] = 'https'

View file

@ -2,6 +2,7 @@
String.include CoreExtensions::String::StripTags
Jekyll::Document.include CoreExtensions::Jekyll::Document::Path
Jekyll::DataReader.include Jekyll::Readers::DataReaderDecorator
# Definir tags de Liquid que provienen de complementos para que siempre
# devuelvan contenido vacío.
@ -37,6 +38,19 @@ end
#
# TODO: Aplicar monkey patches en otro lado...
module Jekyll
Configuration.class_eval do
# No agregar colecciones por defecto, solo las que digamos en base a
# los idiomas. Esto remueve la colección "posts".
#
# Las colecciones de idiomas son agregadas por Site.
#
# @see Site#configuration
# @return [Jekyll::Configuration]
def add_default_collections
self
end
end
Site.class_eval do
def configure_theme
self.theme = nil
@ -57,9 +71,27 @@ module Jekyll
# No necesitamos los archivos estáticos
def retrieve_static_files(_, _); end
# Solo lee los datos
# Solo lee los datos, desde la plantilla y luego desde el sitio,
# usando rutas absolutas, para evitar el uso confuso de Dir.chdir.
#
# Reemplaza jekyll-data también!
#
# @return [Hash]
def read_data
@site.data = DataReader.new(site).read(site.config['data_dir'])
@site.data =
begin
reader = DataReader.new(site)
theme_dir = site.in_theme_dir('_data')
data_dir = site.in_source_dir(site.config['data_dir'])
if theme_dir
reader.read_data_to(theme_dir, reader.content)
reader.read_data_to(data_dir, reader.content)
reader.content
else
reader.read data_dir
end
end
end
# Lee los layouts
@ -175,14 +207,3 @@ module PgSearch
end
end
end
# JekyllData::Reader del plugin jekyll-data modifica Jekyll::Site#reader
# para también leer los datos que vienen en el theme.
module JekyllData
Reader.class_eval do
def read_data
super
read_theme_data
end
end
end

View file

@ -1,11 +1,11 @@
# frozen_string_literal: true
Rails.application.configure do
next unless ENV['RAILS_ENV'] == 'development'
next if Rails.env.test?
domain = ENV.fetch('SUTTY', 'sutty.nl')
config.hosts << domain
config.hosts << "panel.#{domain}"
config.hosts << "api.#{domain}"
config.hosts << /\Aapi\./
end

View file

@ -1,4 +1,5 @@
en:
dark: Dark
dir: ltr
en: English
es: Castellano
@ -171,6 +172,7 @@ en:
usuarie: User
licencia: License
design: Design
indexed_post: Indexed post
attributes:
usuarie:
email: 'E-mail address'
@ -199,6 +201,10 @@ en:
layout_incompatible:
error: "Design can't be changed because there are posts with incompatible layouts"
help: "Your site has posts with layouts only compatible with the current design. If you change it, the site won't work as you expect. If you're trying out designs, you can delete posts in the following incompatible layouts:: %{layouts}."
usuarie:
attributes:
lang:
not_available: "This language is not yet available, would you help us by translating Sutty into it?"
errors:
argument_error: 'Argument `%{argument}` must be an instance of %{class}'
unknown_locale: 'Unknown %{locale} locale'
@ -320,6 +326,10 @@ en:
[Pixelfed](https://pixelfed.social/site/about), and
[others](https://fediverse.party/)) can follow your site,
receive news and interact with them.
There is also the possibility that Sutty advertises your content and/or Fediverse
user automatically to attract followers. You can do it with this
[form](https://cryptpad.fr/form/#/2/form/view/yp1KZwQjgU2RG-zhdQCyw4M8QhftNCVu8e+IJG2iN7Y/)
stats:
index:
title: Statistics
@ -430,7 +440,7 @@ en:
name: "This will be the host name for your site, ie. **example**.sutty.nl. Choose an expression up to 63 characters. It can contain only lowercase letters, numbers and dashes, **and no spaces**. It can't start or end with a dash, or be entirely composed of numbers."
title: 'The title can be anything you want'
description: 'You site description that appears in search engines. Between 50 and 160 characters.'
design: 'Select the design for your site. You can change it later. We add more designs from time to time!'
design: 'Select the design for your site. We add more designs from time to time!'
licencia: 'Everything we publish has automatic copyright. This
means nobody can use our works without explicit permission. By
using licenses, we stablish conditions by which we want to share
@ -481,6 +491,9 @@ en:
success: 'Site upgrade has been completed. Your next build will run this upgrade :)'
error: "There was an error when trying to upgrade your site. This could be due to conflicts that couldn't be solved automatically. A report of the issue has already been sent to our admins. Sorry for the inconvenience! :("
message: 'Skeleton upgrade'
webhooks:
pull:
message: 'Webhooks pull'
footer:
powered_by: 'is developed by'
i18n:
@ -726,3 +739,5 @@ en:
build_stats:
index:
title: "Publications"
indexed_posts:
deleted: "Deleted indexed post %{path} from %{site} (records: %{records})"

View file

@ -1,4 +1,5 @@
es:
dark: Oscuro
es: Castellano
en: English
es-AR: Castellano Rioplatense
@ -171,6 +172,7 @@ es:
usuarie: Usuarie
licencia: Licencia
design: Diseño
indexed_post: Artículo indexado
attributes:
usuarie:
email: 'Correo electrónico'
@ -199,6 +201,10 @@ es:
layout_incompatible:
error: 'No se puede cambiar la plantilla porque hay artículos con formatos incompatibles'
help: 'En tu sitio hay artículos que solo son compatibles con el diseño actual, si cambias la plantilla el sitio no funcionará como esperas. Si estás probando plantillas, puedes eliminar los artículos en los formatos incompatibles: %{layouts}.'
usuarie:
attributes:
lang:
not_available: "Este idioma todavía no está disponible, ¿nos ayudas a agregarlo y mantenerlo?"
errors:
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
unknown_locale: 'El idioma %{locale} es desconocido'
@ -325,6 +331,10 @@ es:
[Pixelfed](https://pixelfed.social/site/about) y
[otros](https://fediverse.party/)) pueden seguir a tu sitio,
recibir novedades e interactuar con ellas.
También existe la posibilidad de que Sutty anuncie tu contenido y/o usuarie del Fediverse
en forma automática para atraer seguidorxs. Podés hacerlo con este
[Formulario](https://cryptpad.fr/form/#/2/form/view/XorL4I-nC17rcEwtol3ghsRDsivfg6g5685MK+TFZ-8/)
stats:
index:
title: Estadísticas
@ -436,7 +446,7 @@ es:
name: 'El nombre de tu sitio que formará parte de la dirección (**ejemplo**.sutty.nl). Solo puede contener hasta 63 letras minúsculas, números y guiones, pero **sin espacios**. No puede empezar ni terminar con guión, ni estar compuesto enteramente por números.'
title: 'El título de tu sitio puede ser lo que quieras.'
description: 'La descripción del sitio, que saldrá en buscadores. Entre 50 y 160 caracteres.'
design: 'Elegí el diseño que va a tener tu sitio aquí. Podés cambiarlo luego. De tanto en tanto vamos sumando diseños nuevos.'
design: 'Elegí el diseño que va a tener tu sitio aquí. De tanto en tanto vamos sumando diseños nuevos.'
licencia: 'Todo lo que publicamos posee automáticamente derechos
de autore. Esto significa que nadie puede hacer uso de nuestras
obras sin permiso explícito. Con las licencias establecemos
@ -489,6 +499,9 @@ es:
success: 'Ya se incorporaron los cambios en el sitio, se aplicarán en la próxima compilación que hagas :)'
error: 'Hubo un error al incorporar los cambios en el sitio. Esto puede deberse a conflictos entre cambios que no se pueden resolver automáticamente. Hemos enviado un reporte del problema a les administradores de Sutty para que estén al tanto de la situación. ¡Lo sentimos! :('
message: 'Actualización del esqueleto'
webhooks:
pull:
message: 'Traer los cambios a partir de un evento remoto'
footer:
powered_by: 'es desarrollada por'
i18n:
@ -734,3 +747,5 @@ es:
build_stats:
index:
title: "Publicaciones"
indexed_posts:
deleted: "Eliminado artículo %{path} de %{site} (filas: %{records})"

View file

@ -17,6 +17,8 @@ Rails.application.routes.draw do
get :'contact/cookie', to: 'invitades#contact_cookie'
post :'contact/:form', to: 'contact#receive', as: :contact
post :'webhooks/pull', to: 'webhooks#pull'
end
end
end
@ -26,9 +28,6 @@ Rails.application.routes.draw do
# alias en nginx sin tener que usar expresiones regulares para
# detectar el nombre del sitio.
get '/sites/private/:site_id(*file)', to: 'private#show', constraints: { site_id: %r{[^/]+} }
# Obtener archivos estáticos desde el directorio público
get '/sites/:site_id/static_file/(*file)', to: 'sites#static_file', as: 'site_static_file',
constraints: { site_id: %r{[^/]+} }
get '/env.js', to: 'env#index'
match '/api/v3/projects/:site_id/notices' => 'api/v1/notices#create', via: %i[post]

View file

@ -0,0 +1,12 @@
class AddTokenToRoles < ActiveRecord::Migration[6.1]
def up
add_column :roles, :token, :string
Rol.find_each do |m|
m.update_column( :token, SecureRandom.hex(64) )
end
end
def down
remove_column :roles, :token
end
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
# Almacenar el último commit indexado
class AddLastIndexedCommitToSites < ActiveRecord::Migration[6.1]
def up
add_column :sites, :last_indexed_commit, :string, null: true
Site.find_each do |site|
site.update_columns(last_indexed_commit: site.repository.head_commit.oid)
rescue Rugged::Error, Rugged::OSError => e
puts "Falló #{site.name}, ignorando: #{e.message}"
end
end
def down
remove_column :sites, :last_indexed_commit
end
end

View file

@ -1,400 +0,0 @@
# This file is auto-generated from the current state of the database. Instead
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
# This file is the source Rails uses to define your schema when running `bin/rails
# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
# be faster and is potentially less error prone than running all of your
# migrations from scratch. Old migrations may fail to apply correctly if those
# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2023_04_15_153231) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_trgm"
enable_extension "pgcrypto"
enable_extension "plpgsql"
create_table "access_logs", id: :uuid, default: nil, force: :cascade do |t|
t.string "host"
t.float "msec"
t.string "server_protocol"
t.string "request_method"
t.string "request_completion"
t.string "uri"
t.string "query_string"
t.integer "status"
t.string "sent_http_content_type"
t.string "sent_http_content_encoding"
t.string "sent_http_etag"
t.string "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"
t.string "http_origin"
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"
t.string "geoip2_data_city_name"
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"
t.string "remote_user"
t.boolean "crawler", default: false
t.string "http_referer"
t.datetime "created_at", precision: 6
t.index ["geoip2_data_city_name"], name: "index_access_logs_on_geoip2_data_city_name"
t.index ["geoip2_data_country_name"], name: "index_access_logs_on_geoip2_data_country_name"
t.index ["host"], name: "index_access_logs_on_host"
t.index ["http_origin"], name: "index_access_logs_on_http_origin"
t.index ["http_user_agent"], name: "index_access_logs_on_http_user_agent"
t.index ["status"], name: "index_access_logs_on_status"
t.index ["uri"], name: "index_access_logs_on_uri"
end
create_table "action_text_rich_texts", force: :cascade do |t|
t.string "name", null: false
t.text "body"
t.string "record_type", null: false
t.bigint "record_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["record_type", "record_id", "name"], name: "index_action_text_rich_texts_uniqueness", unique: true
end
create_table "active_storage_attachments", force: :cascade do |t|
t.string "name", null: false
t.string "record_type", null: false
t.bigint "record_id", null: false
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
end
create_table "active_storage_blobs", force: :cascade do |t|
t.string "key", null: false
t.string "filename", null: false
t.string "content_type"
t.text "metadata"
t.bigint "byte_size", null: false
t.string "checksum", null: false
t.datetime "created_at", null: false
t.string "service_name", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end
create_table "active_storage_variant_records", force: :cascade do |t|
t.bigint "blob_id", null: false
t.string "variation_digest", null: false
t.index ["blob_id", "variation_digest"], name: "index_active_storage_variant_records_uniqueness", unique: true
end
create_table "blazer_audits", force: :cascade do |t|
t.bigint "user_id"
t.bigint "query_id"
t.text "statement"
t.string "data_source"
t.datetime "created_at"
t.index ["query_id"], name: "index_blazer_audits_on_query_id"
t.index ["user_id"], name: "index_blazer_audits_on_user_id"
end
create_table "blazer_checks", force: :cascade do |t|
t.bigint "creator_id"
t.bigint "query_id"
t.string "state"
t.string "schedule"
t.text "emails"
t.text "slack_channels"
t.string "check_type"
t.text "message"
t.datetime "last_run_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["creator_id"], name: "index_blazer_checks_on_creator_id"
t.index ["query_id"], name: "index_blazer_checks_on_query_id"
end
create_table "blazer_dashboard_queries", force: :cascade do |t|
t.bigint "dashboard_id"
t.bigint "query_id"
t.integer "position"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["dashboard_id"], name: "index_blazer_dashboard_queries_on_dashboard_id"
t.index ["query_id"], name: "index_blazer_dashboard_queries_on_query_id"
end
create_table "blazer_dashboards", force: :cascade do |t|
t.bigint "creator_id"
t.text "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["creator_id"], name: "index_blazer_dashboards_on_creator_id"
end
create_table "blazer_queries", force: :cascade do |t|
t.bigint "creator_id"
t.string "name"
t.text "description"
t.text "statement"
t.string "data_source"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["creator_id"], name: "index_blazer_queries_on_creator_id"
end
create_table "build_stats", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "deploy_id"
t.bigint "bytes"
t.float "seconds"
t.string "action", null: false
t.text "log"
t.boolean "status", default: false
t.index ["deploy_id"], name: "index_build_stats_on_deploy_id"
end
create_table "csp_reports", id: :uuid, default: nil, force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
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"
t.integer "column_number"
t.integer "line_number"
t.string "source_file"
end
create_table "deploys", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "site_id"
t.string "type"
t.text "values"
t.index ["site_id"], name: "index_deploys_on_site_id"
t.index ["type"], name: "index_deploys_on_type"
end
create_table "designs", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.text "description"
t.string "gem"
t.string "url"
t.string "license"
t.boolean "disabled", default: false
t.text "credits"
t.string "designer_url"
t.integer "priority"
end
create_table "indexed_posts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.bigint "site_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "locale", default: "simple"
t.string "layout", null: false
t.string "path", null: false
t.string "title", default: ""
t.jsonb "front_matter", default: "{}"
t.string "content", default: ""
t.tsvector "indexed_content"
t.integer "order", default: 0
t.string "dictionary"
t.index ["front_matter"], name: "index_indexed_posts_on_front_matter", using: :gin
t.index ["indexed_content"], name: "index_indexed_posts_on_indexed_content", using: :gin
t.index ["layout"], name: "index_indexed_posts_on_layout"
t.index ["locale"], name: "index_indexed_posts_on_locale"
t.index ["site_id"], name: "index_indexed_posts_on_site_id"
end
create_table "licencias", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.text "description"
t.text "deed"
t.string "url"
t.string "icons"
end
create_table "log_entries", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "site_id"
t.text "text"
t.boolean "sent", default: false
t.index ["site_id"], name: "index_log_entries_on_site_id"
end
create_table "maintenances", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.text "message"
t.datetime "estimated_from"
t.datetime "estimated_to"
t.boolean "are_we_back", default: false
end
create_table "mobility_string_translations", force: :cascade do |t|
t.string "locale", null: false
t.string "key", null: false
t.string "value"
t.string "translatable_type"
t.bigint "translatable_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_string_translations_on_translatable_attribute"
t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_string_translations_on_keys", unique: true
t.index ["translatable_type", "key", "value", "locale"], name: "index_mobility_string_translations_on_query_keys"
end
create_table "mobility_text_translations", force: :cascade do |t|
t.string "locale", null: false
t.string "key", null: false
t.text "value"
t.string "translatable_type"
t.bigint "translatable_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["translatable_id", "translatable_type", "key"], name: "index_mobility_text_translations_on_translatable_attribute"
t.index ["translatable_id", "translatable_type", "locale", "key"], name: "index_mobility_text_translations_on_keys", unique: true
end
create_table "roles", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "site_id"
t.bigint "usuarie_id"
t.string "rol"
t.boolean "temporal"
t.index ["site_id", "usuarie_id"], name: "index_roles_on_site_id_and_usuarie_id", unique: true
t.index ["site_id"], name: "index_roles_on_site_id"
t.index ["usuarie_id"], name: "index_roles_on_usuarie_id"
end
create_table "rollups", force: :cascade do |t|
t.string "name", null: false
t.string "interval", null: false
t.datetime "time", null: false
t.jsonb "dimensions", default: {}, null: false
t.float "value"
t.index ["name", "interval", "time", "dimensions"], name: "index_rollups_on_name_and_interval_and_time_and_dimensions", unique: true
end
create_table "sites", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "name"
t.bigint "design_id"
t.bigint "licencia_id"
t.string "status", default: "waiting"
t.text "description"
t.string "title"
t.boolean "colaboracion_anonima", default: false
t.boolean "contact", default: false
t.string "private_key_ciphertext"
t.boolean "acepta_invitades", default: false
t.string "tienda_api_key_ciphertext", default: ""
t.string "tienda_url", default: ""
t.string "api_key_ciphertext"
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
end
create_table "stats", force: :cascade do |t|
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.bigint "site_id"
t.string "name", null: false
t.index ["name"], name: "index_stats_on_name", using: :hash
t.index ["site_id"], name: "index_stats_on_site_id"
end
create_table "usuaries", force: :cascade do |t|
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.integer "failed_attempts", default: 0, null: false
t.string "unlock_token"
t.datetime "locked_at"
t.boolean "acepta_politicas_de_privacidad", default: false
t.string "invitation_token"
t.datetime "invitation_created_at"
t.datetime "invitation_sent_at"
t.datetime "invitation_accepted_at"
t.integer "invitation_limit"
t.string "invited_by_type"
t.bigint "invited_by_id"
t.integer "invitations_count", default: 0
t.string "lang", default: "es"
t.index ["confirmation_token"], name: "index_usuaries_on_confirmation_token", unique: true
t.index ["email"], name: "index_usuaries_on_email", unique: true
t.index ["invitation_token"], name: "index_usuaries_on_invitation_token", unique: true
t.index ["invitations_count"], name: "index_usuaries_on_invitations_count"
t.index ["invited_by_id"], name: "index_usuaries_on_invited_by_id"
t.index ["invited_by_type", "invited_by_id"], name: "index_usuaries_on_invited_by"
t.index ["reset_password_token"], name: "index_usuaries_on_reset_password_token", unique: true
t.index ["unlock_token"], name: "index_usuaries_on_unlock_token", unique: true
end
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
create_trigger("indexed_posts_before_insert_update_row_tr", :compatibility => 1).
on("indexed_posts").
before(:insert, :update) do
<<-SQL_ACTIONS
new.indexed_content := to_tsvector(('pg_catalog.' || new.dictionary)::regconfig, coalesce(new.title, '') || '
' || coalesce(new.content,''));
SQL_ACTIONS
end
create_trigger("access_logs_before_insert_row_tr", :compatibility => 1).
on("access_logs").
before(:insert) do
"new.created_at := to_timestamp(new.msec);"
end
end

2323
db/structure.sql Normal file

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,9 @@
namespace :cleanup do
desc 'Cleanup sites'
task everything: :environment do
task everything: :environment do |_, args|
before = ENV.fetch('BEFORE', '30').to_i.days.ago
service = CleanupService.new(before: before)
service.cleanup_everything!
CleanupJob.perform_later(before)
end
end

View file

@ -19,6 +19,10 @@ check program stats
every "0 1 * * *"
if status != 0 then alert
check filesystem root with path /
if space usage > 80% for 5 times within 15 cycles then alert
if space usage > 90% for 5 cycles then exec "/usr/bin/foreman run -f /srv/Procfile -d /srv emergency_cleanup" as uid "rails" gid "www-data"
check process que with pidfile /srv/tmp/que.pid
start program = "/usr/bin/foreman run -f /srv/Procfile -d /srv que"
stop program = "/bin/sh -c 'cat /srv/tmp/que.pid | xargs -r kill'"

View file

@ -12,7 +12,8 @@
"@rails/actiontext": "^6.0.0",
"@rails/activestorage": "^6.1.3-1",
"@rails/ujs": "^6.1.3-1",
"@rails/webpacker": "5.2.1",
"@rails/webpacker": "5.4.4",
"@suttyweb/editor": "^0.1.25",
"babel-loader": "^8.2.2",
"chart.js": "^3.5.1",
"chartkick": "^4.0.5",

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