mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-22 16:46:22 +00:00
Merge branch 'rails' into issue-13077
This commit is contained in:
commit
06bde9f971
159 changed files with 6391 additions and 3066 deletions
|
@ -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
1
.env.development
Normal file
|
@ -0,0 +1 @@
|
|||
HAINISH=../haini.sh/haini.sh
|
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -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/
|
||||
|
|
111
.gitlab-ci.yml
111
.gitlab-ci.yml
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
7
Gemfile
7
Gemfile
|
@ -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'
|
||||
|
|
15
Gemfile.lock
15
Gemfile.lock
|
@ -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
140
Makefile
|
@ -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
|
1
Procfile
1
Procfile
|
@ -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
|
||||
|
|
51
README.md
51
README.md
|
@ -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
185
Taskfile.yaml
Normal 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"
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
28
app/assets/stylesheets/dark.scss
Normal file
28
app/assets/stylesheets/dark.scss
Normal 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;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
.editor {
|
||||
.old.editor {
|
||||
box-sizing: border-box;
|
||||
*, *::before, *::after { box-sizing: inherit; }
|
||||
|
||||
|
|
22
app/assets/stylesheets/new_editor.scss
Normal file
22
app/assets/stylesheets/new_editor.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -9,10 +9,10 @@ module Api
|
|||
# Generar un stacktrace en segundo plano y enviarlo por correo
|
||||
# solo si la API key es verificable. Del otro lado siempre
|
||||
# respondemos con lo mismo.
|
||||
def create
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
77
app/controllers/api/v1/webhooks_controller.rb
Normal file
77
app/controllers/api/v1/webhooks_controller.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,3 +6,4 @@ import './prosemirror'
|
|||
import './timezone'
|
||||
import './turbolinks-anchors'
|
||||
import './validation'
|
||||
import './new_editor'
|
||||
|
|
14
app/javascript/etc/new_editor.js
Normal file
14
app/javascript/etc/new_editor.js
Normal 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
8
app/jobs/cleanup_job.rb
Normal 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
|
|
@ -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
16
app/jobs/git_pull_job.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
36
app/lib/jekyll/readers/data_reader_decorator.rb
Normal file
36
app/lib/jekyll/readers/data_reader_decorator.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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
|
||||
|
|
4
app/models/metadata_new_content.rb
Normal file
4
app/models/metadata_new_content.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Testear el nuevo editor
|
||||
class MetadataNewContent < MetadataContent; end
|
4
app/models/metadata_new_html.rb
Normal file
4
app/models/metadata_new_html.rb
Normal file
|
@ -0,0 +1,4 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Campos en HTML con el nuevo editor
|
||||
class MetadataNewHtml < MetadataHtml; end
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
66
app/policies/indexed_post_policy.rb
Normal file
66
app/policies/indexed_post_policy.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
3
app/views/posts/attribute_ro/_new_content.haml
Normal file
3
app/views/posts/attribute_ro/_new_content.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%tr{ id: attribute }
|
||||
%th= post_label_t(attribute, post: post)
|
||||
%td{ lang: locale, dir: dir }= metadata.value
|
3
app/views/posts/attribute_ro/_new_html.haml
Normal file
3
app/views/posts/attribute_ro/_new_html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%tr{ id: attribute }
|
||||
%th= post_label_t(attribute, post: post)
|
||||
%td{ lang: locale, dir: dir }= metadata.value.html_safe
|
|
@ -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
|
||||
|
|
9
app/views/posts/attributes/_new_content.haml
Normal file
9
app/views/posts/attributes/_new_content.haml
Normal 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'
|
6
app/views/posts/attributes/_new_html.haml
Normal file
6
app/views/posts/attributes/_new_html.haml
Normal 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)
|
|
@ -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
|
||||
|
|
|
@ -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|
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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
8
bin/modified_files
Executable 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
4
bin/with_extension
Executable file
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
|
||||
grep "\.${1}$"
|
||||
exit 0
|
|
@ -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
252
config/brakeman.ignore
Normal 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"
|
||||
}
|
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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})"
|
||||
|
|
|
@ -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})"
|
||||
|
|
|
@ -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]
|
||||
|
|
12
db/migrate/20230731195050_add_token_to_roles.rb
Normal file
12
db/migrate/20230731195050_add_token_to_roles.rb
Normal 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
|
|
@ -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
|
400
db/schema.rb
400
db/schema.rb
|
@ -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
2323
db/structure.sql
Normal file
File diff suppressed because it is too large
Load diff
|
@ -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
|
||||
|
|
|
@ -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'"
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue