sitios a base de datos

This commit is contained in:
f 2019-07-03 20:25:23 -03:00
parent ba6688430a
commit 78820b6fac
No known key found for this signature in database
GPG key ID: 2AE5A13E321F953D
25 changed files with 164 additions and 283 deletions

1
.gitignore vendored
View file

@ -26,3 +26,4 @@
/_deploy/*
/_usuarias/*
/_invitadxs/*
.env

View file

@ -15,22 +15,42 @@ Metrics/AbcSize:
- 'db/schema.rb'
- 'db/migrate/*.rb'
- 'app/models/site.rb'
- 'app/controllers/sites_controller.rb'
- 'app/controllers/posts_controller.rb'
- 'app/controllers/invitadxs_controller.rb'
Metrics/MethodLength:
Exclude:
- 'db/schema.rb'
- 'db/migrate/*.rb'
- 'app/models/site.rb'
- 'app/controllers/sites_controller.rb'
- 'app/controllers/posts_controller.rb'
- 'app/controllers/invitadxs_controller.rb'
Metrics/BlockLength:
Exclude:
- 'config/environments/production.rb'
- 'config/initializers/devise.rb'
- 'db/schema.rb'
Metrics/ClassLength:
Exclude:
- 'app/models/site.rb'
- 'app/controllers/posts_controller.rb'
Performance/TimesMap:
Exclude:
- 'app/models/site.rb'
Lint/HandleExceptions:
Exclude:
- 'app/controllers/posts_controller.rb'
Style/GuardClause:
Exclude:
- 'app/controllers/posts_controller.rb'
Metrics/PerceivedComplexity:
Exclude:
- 'app/controllers/posts_controller.rb'

View file

@ -15,14 +15,11 @@ class ApplicationController < ActionController::Base
private
# Encontrar un sitio por su ID
# TODO volverlo más natural a rails
# Encontrar un sitio por su nombre
def find_site
id = params[:site_id] || params[:id]
current_user.sites.find do |s|
s.id == id
end
current_usuarie.sites.find_by_name id
end
def find_post(site)

View file

@ -1,8 +1,9 @@
# frozen_string_literal: true
# Controlador de traducciones
class I18nController < ApplicationController
include Pundit
before_action :authenticate!
before_action :authenticate_usuarie!
def index
authorize :i18n

View file

@ -1,10 +1,13 @@
# frozen_string_literal: true
# Controlador de Invitadxs
#
# TODO: reemplazar
class InvitadxsController < ApplicationController
include Pundit
def index
authenticate!
authenticate_usuarie!
@site = find_site
@invitadxs = @site.invitadxs
@ -23,7 +26,8 @@ class InvitadxsController < ApplicationController
@invitadx.sites << @site
if @invitadx.save
InvitadxMailer.with(site: @site, invitadx: @invitadx).confirmation_required.deliver
InvitadxMailer.with(site: @site, invitadx: @invitadx)
.confirmation_required.deliver
redirect_to site_invitadx_path(@site, @invitadx)
else
render 'new'
@ -42,7 +46,7 @@ class InvitadxsController < ApplicationController
site.id == params[:site_id]
end
if @invitadx.confirmation_token = params[:confirmation_token]
if (@invitadx.confirmation_token = params[:confirmation_token])
@invitadx.update_attribute :confirmed, true
flash[:info] = t('.confirmed')
redirect_to site_invitadxs_login_new_path(@site)
@ -55,6 +59,7 @@ class InvitadxsController < ApplicationController
def invitadx_params
params.require(:invitadx).permit(:email, :password,
:password_confirmation, :acepta_politicas_de_privacidad)
:password_confirmation,
:acepta_politicas_de_privacidad)
end
end

View file

@ -1,36 +0,0 @@
# frozen_string_literal: true
class LoginController < ApplicationController
protect_from_forgery with: :exception
def index
redirect_to new_login_path
end
def new
@has_cover = true
@site = Site.find(params[:site_id]) if params[:site_id].present?
render 'login/new'
end
def create
authenticate
session[:lang] = params[:lang]
referer = request.referer unless %r{/login} =~ request.referer
referer = params[:referer] if params[:referer].present?
if authenticated?
redirect_to referer || sites_path
else
flash[:danger] = t('login.error')
render 'login/new'
end
end
def delete
warden.logout
redirect_to login_new_path
end
end

View file

@ -1,15 +1,17 @@
# frozen_string_literal: true
# Controlador para artículos
class PostsController < ApplicationController
include Pundit
before_action :authenticate!
before_action :authenticate_usuarie!
def index
authorize Post
@site = find_site
@lang = find_lang(@site)
@category = session[:category] = params.dig(:category)
@posts = policy_scope(@site.posts_for(@lang), policy_scope_class: PostPolicy::Scope)
@posts = policy_scope(@site.posts_for(@lang),
policy_scope_class: PostPolicy::Scope)
if params[:sort_by].present?
begin
@ -49,7 +51,8 @@ class PostsController < ApplicationController
# El post se guarda como incompleto si se envió con "guardar para
# después"
@post.update_attributes(incomplete: params[:commit_incomplete].present?)
@post.update_attributes(incomplete:
params[:commit_incomplete].present?)
# Las usuarias pueden especificar una autora, de la contrario por
# defecto es la usuaria actual
@ -59,7 +62,10 @@ class PostsController < ApplicationController
# Todo lo que crean lxs invitadxs es borrador
@post.update_attributes(draft: true)
end
@post.update_attributes(author: current_user.username) unless @post.author
unless @post.author
@post.update_attributes(author:
current_user.username)
end
if @post.save
if @post.incomplete?
@ -94,7 +100,9 @@ class PostsController < ApplicationController
# Solo las usuarias pueden modificar la autoría
if current_user.is_a? Usuaria
@post.update_attributes(author: params[:post][:author]) if params[:post][:author].present?
if params[:post][:author].present?
@post.update_attributes(author: params[:post][:author])
end
@post.update_attributes(draft: false)
else
# Todo lo que crean lxs invitadxs es borrador
@ -103,7 +111,9 @@ class PostsController < ApplicationController
if @post.save
flash[:success] = @site.config.dig('thanks')
redirect_to site_posts_path(@site, category: session[:category], lang: @lang)
redirect_to site_posts_path(@site,
category: session[:category],
lang: @lang)
else
render 'posts/edit'
end
@ -119,7 +129,8 @@ class PostsController < ApplicationController
@post.destroy
redirect_to site_posts_path(@site, category: session[:category], lang: @lang)
redirect_to site_posts_path(@site, category: session[:category],
lang: @lang)
end
private

View file

@ -3,12 +3,12 @@
# Controlador de sitios
class SitesController < ApplicationController
include Pundit
before_action :authenticate!
before_action :authenticate_usuarie!
# Ver un listado de sitios
def index
authorize Site
@sites = current_user.sites
@sites = current_usuarie.sites
end
# No tenemos propiedades de un sitio aún, así que vamos al listado de
@ -25,7 +25,8 @@ class SitesController < ApplicationController
authorize Site
@site = find_site
file = [params[:basename], params[:format]].join('.')
path = Pathname.new(File.join(@site.path, 'public', params[:type], file))
path = File.join(@site.path, 'public', params[:type], file)
path = Pathname.new path
# TODO: Verificar que no nos estén sacando archivos del sistema, como
# /etc/passwd
@ -63,11 +64,12 @@ class SitesController < ApplicationController
authorize @site
lang = params.require(:posts).require(:lang)
result = if params[:posts][:force].present?
@site.reorder_collection! lang
else
@site.reorder_collection(lang, params.require(:posts).require(:order))
end
if params[:posts][:force].present?
result = @site.reorder_collection! lang
else
result = @site
.reorder_collection(lang, params.require(:posts).require(:order))
end
if result
flash[:info] = I18n.t('info.posts.reorder')

View file

@ -1,6 +0,0 @@
# frozen_string_literal: true
module Warden
module EmailAndPassword
end
end

View file

@ -1,34 +0,0 @@
# frozen_string_literal: true
require 'email_address'
module Warden
module EmailAndPassword
class Strategy < Warden::Strategies::Base
def valid?
return false unless params.include? 'username'
return false unless params.include? 'password'
username = params['username']
@email = EmailAddress.new(username, host_validation: :a)
Rails.logger.error [username, @email.error].join(': ') unless @email.valid?
@email.valid?
end
# Autentica a una posible invitadx, no fallamos para que haya
# fallback con IMAP
def authenticate!
u = ::Invitadx.find_by_email(params['username'])
return unless u.try(:authenticate, params['password'])
if u.confirmed?
success! u
else
fail! 'unconfirmed'
end
end
end
end
end

View file

@ -1,6 +0,0 @@
# frozen_string_literal: true
module Warden
module IMAP
end
end

View file

@ -1,57 +0,0 @@
# frozen_string_literal: true
require 'net/imap'
require 'warden'
require 'email_address'
module Warden
module IMAP
# Una estrategia de autenticación por IMAP
class Strategy < Warden::Strategies::Base
def valid?
return false unless params.include? 'username'
return false unless params.include? 'password'
username = params['username']
@email = EmailAddress.new(username, host_validation: :a)
Rails.logger.error [username, @email.error].join(': ') unless @email.valid?
@email.valid?
end
def authenticate!
imap_connect
imap_login
end
private
def imap_connect
# No vamos a enviar la contraseña en texto plano a ningún lado
@imap = Net::IMAP.new(ENV.fetch('IMAP_SERVER', 'kefir.red'), ssl: true)
# Errores más comunes según
# https://ruby-doc.org/stdlib-2.0.0/libdoc/net/imap/rdoc/Net/IMAP.html
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::ENETUNREACH,
SocketError, Net::IMAP::ByeResponseError => e
Rails.logger.error e.to_s
@imap.disconnect
fail! e.to_s
end
def imap_login
Rails.logger.info "Autenticando a #{@email.normal}"
@imap.login(@email.normal, params['password'])
@imap.disconnect
success! ::Usuaria.find(@email.normal)
rescue Net::IMAP::NoResponseError, EOFError => e
@imap.disconnect
Rails.logger.error e.to_s
fail! e.to_s
end
end
end
end

View file

@ -7,8 +7,22 @@ class Site < ApplicationRecord
has_and_belongs_to_many :invitades, class_name: 'Usuarie',
join_table: 'invitades_sites'
after_initialize :load_jekyll!
attr_accessor :jekyll, :collections
attr_reader :path
def load_jekyll!
Dir.chdir(path) do
@jekyll ||= Site.load_jekyll(Dir.pwd)
end
end
# Traer la ruta del sitio
#
# Equivale a _sites + nombre
def path
@path ||= File.join(Site.site_path, name)
end
# Este sitio acepta invitadxs?
def invitadxs?
@ -121,6 +135,7 @@ class Site < ApplicationRecord
# idioma actual
collection = default_lang if collection == 'posts' && i18n?
@collections ||= {}
c = @collections[collection]
return c if c

View file

@ -1,13 +1,16 @@
# frozen_string_literal: true
class I18nPolicy < SuttyPolicy
# Política de acceso a la traducción del sitio.
#
# TODO: Prohibir Invitades
class I18nPolicy
def initialize(usuarix, _i18n)
@usuarix = usuarix
end
# Solo las usuarias
def index?
usuaria?
true
end
def edit?
@ -15,6 +18,6 @@ class I18nPolicy < SuttyPolicy
end
def update?
usuaria?
true
end
end

View file

@ -1,5 +1,8 @@
# frozen_string_literal: true
# Política de acceso para Invitades
#
# TODO: Incorporar a Usuarie y eliminar
class InvitadxPolicy
attr_reader :usuarix, :model
@ -14,7 +17,10 @@ class InvitadxPolicy
# Al crear, el modelo recibido es un sitio
def create?
raise ArgumentError, "#{model.class} must be Site" unless model.class == Site
unless model.class == Site
raise ArgumentError,
"#{model.class} must be Site"
end
# El sitio acepta invitadxs
model.invitadxs?

View file

@ -1,16 +1,19 @@
# frozen_string_literal: true
class SitePolicy < SuttyPolicy
attr_reader :site
# Política de acceso para sitios
#
# TODO: Distinguir entre Invitades
class SitePolicy
attr_reader :site, :usuarie
def initialize(usuarix, site)
@usuarix = usuarix
def initialize(usuarie, site)
@usuarie = usuarie
@site = site
end
# Solo las usuarias
def index?
usuaria?
true
end
# Todxs lxs usuarixs pueden ver el sitio
@ -20,7 +23,7 @@ class SitePolicy < SuttyPolicy
# Solo las usuarias
def build?
usuaria?
true
end
def send_public_file?
@ -28,14 +31,14 @@ class SitePolicy < SuttyPolicy
end
def enqueue?
usuaria?
true
end
def build_log?
usuaria?
true
end
def reorder_posts?
usuaria?
true
end
end

View file

@ -1,22 +0,0 @@
# frozen_string_literal: true
class SuttyPolicy
attr_reader :usuarix
def invitadx?
usuarix.is_a? Invitadx
end
def usuaria?
usuarix.is_a? Usuaria
end
class Scope < SuttyPolicy
attr_reader :scope
def initialize(usuarix, scope)
@usuarix = usuarix
@scope = scope
end
end
end

View file

@ -1,6 +1,10 @@
%nav{'aria-label': 'breadcrumb', role: 'navigation'}
%ol.breadcrumb
%li.breadcrumb-item= render 'login/logout'
%li.breadcrumb-item
= link_to destroy_usuarie_session_path, method: :delete,
data: { toggle: 'tooltip' }, title: t('help.logout'),
role: 'button', class: 'btn-text' do
= fa_icon 'sign-out', title: t('help.logout')
- if help = @site.try(:config).try(:dig, 'help')
%li.breadcrumb-item= link_to t('.help'), help, target: '_blank'
- crumbs.compact.each do |crumb|

View file

@ -1,4 +0,0 @@
= form_tag login_logout_path, method: :delete, class: 'form-inline' do
= button_tag type: 'submit', class: 'btn-text',
data: { toggle: 'tooltip' }, title: t('help.logout') do
= fa_icon('sign-out')

View file

@ -1,18 +0,0 @@
.row.align-items-center.justify-content-center.full-height
.col-md-6.align-self-center
= render 'layouts/flash'
= form_tag login_path do
- if @site
%input{type: 'hidden', name: 'referer', value: site_path(@site)}
.form-group
%input{type: 'email', name: 'username', class: 'form-control', placeholder: t('login.email')}
.form-group
%input{type: 'password', name: 'password', class: 'form-control', placeholder: t('login.password')}
.form-group
%select.form-control{name: 'lang', placeholder: t('login.lang')}
%option{value: 'es'} Castellano
%option{value: 'en'} English
.form-group
%input{type: 'submit', value: t('login.submit'), class: 'btn btn-lg btn-primary btn-block'}

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Settings specified here will take precedence over those in
# config/application.rb.
# Code is not reloaded between requests.
config.cache_classes = true
@ -21,9 +22,10 @@ Rails.application.configure do
# `config/secrets.yml.key`.
config.read_encrypted_secrets = true
# Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this.
config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Disable serving static files from the `/public` folder by default
# since Apache or NGINX already handles this.
config.public_file_server
.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
# Compress JavaScripts and CSS.
config.assets.js_compressor = :uglifier
@ -32,20 +34,22 @@ Rails.application.configure do
# Do not fallback to assets pipeline if a precompiled asset is missed.
config.assets.compile = false
# `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb
# `config.assets.precompile` and `config.assets.version` have moved to
# config/initializers/assets.rb
# Enable serving of images, stylesheets, and JavaScripts from an asset server.
# Enable serving of images, stylesheets, and JavaScripts from an asset
# server.
# config.action_controller.asset_host = 'http://assets.example.com'
# Specifies the header that your server uses for sending files.
# config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX
config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect'
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
# Force all access to the app over SSL, use Strict-Transport-Security,
# and use secure cookies.
config.force_ssl = true
# Use the lowest log level to ensure availability of diagnostic information
# when problems arise.
# Use the lowest log level to ensure availability of diagnostic
# information when problems arise.
config.log_level = :debug
# Prepend all log lines with the following tags.
@ -54,28 +58,32 @@ Rails.application.configure do
# Use a different cache store in production.
# config.cache_store = :mem_cache_store
# Use a real queuing backend for Active Job (and separate queues per environment)
# Use a real queuing backend for Active Job (and separate queues per
# environment)
# config.active_job.queue_adapter = :resque
# config.active_job.queue_name_prefix = "sutty_#{Rails.env}"
config.action_mailer.perform_caching = false
# Ignore bad email addresses and do not raise email delivery errors.
# Set this to true and configure the email server for immediate delivery to raise delivery errors.
# Set this to true and configure the email server for immediate
# delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation cannot be found).
# Enable locale fallbacks for I18n (makes lookups for any locale fall
# back to the I18n.default_locale when a translation cannot be found).
config.i18n.fallbacks = true
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
# Use default logging formatter so that PID and timestamp are not suppressed.
# Use default logging formatter so that PID and timestamp are not
# suppressed.
config.log_formatter = ::Logger::Formatter.new
# Use a different logger for distributed setups.
# require 'syslog/logger'
# config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name')
require 'syslog/logger'
config.logger = ActiveSupport::TaggedLogging
.new(Syslog::Logger.new('app-name'))
if ENV['RAILS_LOG_TO_STDOUT'].present?
logger = ActiveSupport::Logger.new(STDOUT)
@ -87,16 +95,16 @@ Rails.application.configure do
config.active_record.dump_schema_after_migration = false
# Recibir por mail notificaciones de excepciones
config.action_mailer.default_url_options = { host: 'sutty.kefir.red' }
config.action_mailer.default_url_options = { host: ENV['SUTTY'] }
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
config.action_mailer.delivery_method = :sendmail
config.action_mailer.default_options = { from: ENV.fetch('DEFAULT_FROM', 'sutty@kefir.red') }
config.action_mailer.default_options = { from: ENV['DEFAULT_FROM'] }
config.middleware.use ExceptionNotification::Rack,
email: {
email_prefix: '[ERROR]',
sender_address: %(sutty@kefir.red),
exception_recipients: 'sysadmin@kefir.red'
sender_address: ENV['DEFAULT_FROM'],
exception_recipients: ENV['EXCEPTION_TO']
}
end

View file

@ -1,7 +1,8 @@
# frozen_string_literal: true
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Settings specified here will take precedence over those in
# config/application.rb.
# The test environment is used exclusively to run your application's
# test suite. You never need to work with it otherwise. Remember that
@ -9,16 +10,17 @@ Rails.application.configure do
# and recreated between test runs. Don't rely on the data there!
config.cache_classes = true
# Do not eager load code on boot. This avoids loading your whole application
# just for the purpose of running a single test. If you are using a tool that
# preloads Rails for running tests, you may have to set it to true.
# Do not eager load code on boot. This avoids loading your whole
# application just for the purpose of running a single test. If you
# are using a tool that preloads Rails for running tests, you may have
# to set it to true.
config.eager_load = false
# Configure public file server for tests with Cache-Control for performance.
# Configure public file server for tests with Cache-Control for
# performance.
config.public_file_server.enabled = true
config.public_file_server.headers = {
'Cache-Control' => "public, max-age=#{1.hour.seconds.to_i}"
}
config.public_file_server.headers = { 'Cache-Control' => "public,
max-age=#{1.hour.seconds.to_i}" }
# Show full error reports and disable caching.
config.consider_all_requests_local = true
@ -35,6 +37,8 @@ Rails.application.configure do
# The :test delivery method accumulates sent emails in the
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
config.action_mailer.default_url_options = { host: 'localhost',
port: 3000 }
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr

View file

@ -1,19 +0,0 @@
# frozen_string_literal: true
Rails.configuration.middleware.use RailsWarden::Manager do |manager|
manager.default_strategies :email, :imap
manager.failure_app = ->(env) { LoginController.action(:new).call(env) }
end
class Warden::SessionSerializer
def serialize(record)
[record.username]
end
def deserialize(keys)
Invitadx.find_by_email(keys.first) || Usuaria.find(keys.first)
end
end
Warden::Strategies.add(:imap, Warden::IMAP::Strategy)
Warden::Strategies.add(:email, Warden::EmailAndPassword::Strategy)

View file

@ -1,11 +1,9 @@
# frozen_string_literal: true
Rails.application.routes.draw do
root 'application#index'
devise_for :usuaries
get 'login/new', to: 'login#new'
post 'login', to: 'login#create'
delete 'login/logout', to: 'login#delete'
root 'application#index'
get 'markdown', to: 'application#markdown'
@ -13,15 +11,16 @@ Rails.application.routes.draw do
# como un objeto válido
resources :invitadxs, only: [:create]
resources :sites, only: %i[index show], constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do
resources :sites, only: %i[index show],
constraints: { site_id: %r{[^/]+}, id: %r{[^/]+} } do
get 'public/:type/:basename', to: 'sites#send_public_file'
resources :posts
resources :templates
resources :invitadxs, only: %i[index new show] do
get :confirmation, to: 'invitadxs#confirmation'
end
get :'invitadxs/login/new', to: 'login#new'
post :'invitadxs/login', to: 'login#create'
get 'i18n', to: 'i18n#index'
get 'i18n/edit', to: 'i18n#edit'

View file

@ -42,13 +42,17 @@ class CreateSitios < ActiveRecord::Migration[5.2]
usuarias.each do |email|
usuarie = Usuarie.find_by(email: email)
usuarie ||= Usuarie.create(email: email,
password: SecureRandom.hex)
password: SecureRandom.hex,
confirmed_at: Date.today)
site.usuaries << usuarie
end
# Les Invitades ya están migrades
invitadxs.each do |email|
site.invitades << Usuarie.find_by(email: email)
usuarie = Usuarie.find_by(email: email)
usuarie ||= Usuarie.create(email: email,
password: SecureRandom.hex,
confirmed_at: Date.today)
site.invitades << usuarie
end
end
end