wip de invitadxs

This commit is contained in:
f 2018-09-28 11:34:37 -03:00
parent 2e4219ce07
commit 39b575ebfb
No known key found for this signature in database
GPG key ID: F3FDAB97B5F9F7E7
23 changed files with 368 additions and 19 deletions

View file

@ -25,7 +25,7 @@ gem 'turbolinks', '~> 5'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
gem 'jbuilder', '~> 2.5'
# Use ActiveModel has_secure_password
# gem 'bcrypt', '~> 3.1.7'
gem 'bcrypt', '~> 3.1.7'
# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

View file

@ -45,6 +45,7 @@ GEM
arel (8.0.0)
autoprefixer-rails (9.1.4)
execjs
bcrypt (3.1.12)
bcrypt_pbkdf (1.0.0)
bindex (0.5.0)
bootstrap (4.0.0)
@ -302,6 +303,7 @@ PLATFORMS
ruby
DEPENDENCIES
bcrypt (~> 3.1.7)
bcrypt_pbkdf
bootstrap (~> 4.0.0)
capistrano

View file

@ -0,0 +1,53 @@
class InvitadxsController < ApplicationController
include Pundit
def index
authenticate!
@site = find_site
@invitadxs = @site.invitadxs
end
def new
@site = Site.find(params[:site_id])
@has_cover = true
@invitadx = Invitadx.new
end
def create
@site = Site.find(params[:invitadx][:site])
@invitadx = Invitadx.new(invitadx_params)
@invitadx.confirmation_token = SecureRandom.hex(16)
@invitadx.sites << @site
if @invitadx.save
InvitadxMailer.with(site: @site, invitadx: @invitadx).confirmation_required.deliver
redirect_to invitadx_path(@invitadx)
else
render 'new'
end
end
def show
@has_cover = true
@invitadx = Invitadx.find(params[:id])
end
def confirmation
@invitadx = Invitadx.find(params[:invitadx_id])
@site = @invitadx.sites.find do |site|
site.id == params[:site_id]
end
if @invitadx.confirmation_token = params[:confirmation_token]
@invitadx.update_attribute :confirmed, true
redirect_to site_
end
end
private
def invitadx_params
params.require(:invitadx).permit(:email, :password, :password_confirmation)
end
end

View file

@ -0,0 +1,7 @@
class InvitadxMailer < ApplicationMailer
def confirmation_required
@invitadx = params[:invitadx]
@site = params[:site]
mail to: @invitadx.email, subject: t('.subject')
end
end

76
app/models/invitadx.rb Normal file
View file

@ -0,0 +1,76 @@
class Invitadx < ApplicationRecord
has_secure_password
validates_uniqueness_of :email
after_create :create_invitadx_directory!
after_save :add_sites!
# Para facilitar la serialización de Warden
def username
email
end
def path
File.join(Rails.root, '_invitadxs', email)
end
# TODO convertir el Pathname en un helper
def site_dirs
return [] unless Dir.exists? path
Pathname.new(path).children.map(&:expand_path).map(&:to_s)
end
def sites
@sites ||= site_dirs.map do |site|
Site.find(File.basename(site))
end.compact
end
private
# Crea el directorio en _sites
def create_invitadx_directory!
FileUtils.mkdir_p(path)
end
# Agrega el sitio a las invitadxs y viceversa
def add_sites!
sites.each do |site|
dir = File.join(path, site.name)
file = site.invitadxs_file
unless File.exists? dir
FileUtils.ln_s File.join('..', '..', '_sites', site.name), dir
end
if File.exists? file
invitadxs = File.read(file).split("\n")
invitadxs_orig = invitadxs.dup
invitadxs << email unless invitadxs.include? email
else
invitadxs_orig = []
invitadxs = [email]
end
# Solo escribir los cambios si hubo cambios en la lista
return if invitadxs_orig == invitadxs
r = File.open(file, File::RDWR | File::CREAT, 0o640) do |f|
# Bloquear el archivo para que no sea accedido por otro
# proceso u otra editora
f.flock(File::LOCK_EX)
# Empezar por el principio
f.rewind
# Escribir la fecha de creación
f.write(invitadxs.join("\n"))
# Eliminar el resto
f.flush
f.truncate(f.pos)
end
end
end
end

View file

@ -11,6 +11,15 @@ class Site
@collections = {}
end
# Este sitio acepta invitadxs?
def invitadxs?
jekyll.config.fetch('invitadxs', false)
end
def cover
"/covers/#{name}.png"
end
# Determina si el sitio está en varios idiomas
def i18n?
!translations.empty?
@ -173,6 +182,16 @@ class Site
end
end
def invitadxs_file
File.join(path, '.invitadxs')
end
def invitadxs
@invitadxs ||= File.read(invitadxs_file).split("\n").map do |i|
Invitadx.find_by_email(i)
end
end
def failed_file
File.join(path, '.failed')
end

View file

@ -0,0 +1,5 @@
%h1= t('.hi')
%p= t('.body')
%code= site_invitadx_confirmation_url(@site, @invitadx, confirmation_token: @invitadx.confirmation_token)

View file

@ -0,0 +1,5 @@
= t('.hi')
= t('.body')
= site_invitadx_confirmation_url(@site, @invitadx, confirmation_token: @invitadx.confirmation_token)

View file

@ -0,0 +1,13 @@
.row
.col
= render 'layouts/breadcrumb', crumbs: [ t('sites.index'), t('.title') ]
.row
.col
%h1= t('.title')
%table.table.table-striped.table-condensed
%tbody
- @invitadxs.each do |invitadx|
%tr
%td= invitadx.email
%td= invitadx.created_at

View file

@ -0,0 +1,31 @@
.row.align-items-center.justify-content-center.full-height
.col-md-6.align-self-center
- if @invitadx.errors.full_messages.empty?
.alert.alert-dismissible.alert-info.fade.show{role: 'alert'}
= @site.config.dig('welcome', 'message') || t('.welcome')
%button.close{type: 'button',
data: { dismiss: 'alert' },
'aria-label': t('help.close') }
%span{'aria-hidden': true} &times;
- else
.alert.alert-dismissible.alert-info.fade.show{role: 'alert'}
%ul
- @invitadx.errors.full_messages.each do |message|
%li= message
%button.close{type: 'button',
data: { dismiss: 'alert' },
'aria-label': t('help.close') }
%span{'aria-hidden': true} &times;
= form_for @invitadx do |f|
= f.hidden_field :site, value: @site.id
.form-group
= f.email_field :email, class: 'form-control', placeholder: t('.email')
.form-group
= f.password_field :password, class: 'form-control', placeholder: t('.password')
.form-group
= f.password_field :password_confirmation, class: 'form-control', placeholder: t('.password_confirmation')
.form-group
- button = @site.config.dig('welcome', 'button')
= f.submit t('.submit'), class: 'btn btn-lg btn-primary btn-block', style: (button) ? "background-color: #{button};" : ''

View file

@ -0,0 +1,3 @@
.row.align-items-center.justify-content-center.full-height
.col-md-6.align-self-center
= t('.confirmation_sent')

View file

@ -6,7 +6,7 @@
= csrf_meta_tags
= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track': 'reload'
= javascript_include_tag 'application', 'data-turbolinks-track': 'reload'
%body{class: @has_cover ? 'background-cover' : ''}
%body{class: @has_cover ? 'background-cover' : '', style: @has_cover ? "background-image: url(#{@site.try(:cover)})": ''}
.container-fluid
= yield
%footer.footer

View file

@ -0,0 +1,3 @@
%html
%body
= yield

View file

@ -0,0 +1 @@
= yield

View file

@ -1,16 +1,9 @@
# Be sure to restart your server when you modify this file.
ActiveSupport::Inflector.inflections(:en) do |inflect|
inflect.plural 'invitadx', 'invitadxs'
inflect.singular 'invitadxs', 'invitadx'
end
# Add new inflection rules using the following format. Inflections
# are locale specific, and you may define rules for as many different
# locales as you wish. All of these examples are active by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.plural /^(ox)$/i, '\1en'
# inflect.singular /^(ox)en/i, '\1'
# inflect.irregular 'person', 'people'
# inflect.uncountable %w( fish sheep )
# end
# These inflection rules are supported but not enabled by default:
# ActiveSupport::Inflector.inflections(:en) do |inflect|
# inflect.acronym 'RESTful'
# end
ActiveSupport::Inflector.inflections(:es) do |inflect|
inflect.plural 'invitadx', 'invitadxs'
inflect.singular 'invitadxs', 'invitadx'
end

View file

@ -1,7 +1,8 @@
require 'warden/imap'
require 'warden/email_and_password'
Rails.configuration.middleware.use RailsWarden::Manager do |manager|
manager.default_strategies :imap
manager.default_strategies :email, :imap
manager.failure_app = -> (env) { LoginController.action(:new).call(env) }
end
@ -11,8 +12,9 @@ class Warden::SessionSerializer
end
def deserialize(keys)
Usuaria.find(keys.first)
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,4 +1,13 @@
en:
activerecord:
errors:
models:
invitadx:
attributes:
email:
taken: 'This e-mail address is already taken, please choose another'
password_confirmation:
confirmation: The passwords don't match
errors:
argument_error: 'Argument `%{argument}` must be an instance of %{class}'
unknown_locale: 'Unknown %{locale} locale'
@ -6,6 +15,14 @@ en:
reorder: "We're sorry, we couldn't reorder the articles"
disordered: "The posts are disordered, this will prevent you from reordering them!"
disordered_button: 'Reorder!'
invitadxs:
index:
title: Guests
new:
email: E-Mail
password: Password
password_confirmation: Repeat password
submit: Register
info:
posts:
reorder: "The articles have been reordered!"

View file

@ -1,4 +1,13 @@
es:
activerecord:
errors:
models:
invitadx:
attributes:
email:
taken: 'Esa cuenta ya está tomada, por favor elige otra.'
password_confirmation:
confirmation: Las contraseñas no coinciden
errors:
argument_error: 'El argumento `%{argument}` debe ser una instancia de %{class}'
unknown_locale: 'El idioma %{locale} es desconocido'
@ -6,6 +15,14 @@ es:
reorder: "Lo sentimos, no pudimos reordenar los artículos."
disordered: 'Los artículos no tienen número de orden, esto impedirá que los puedas reordenar'
disordered_button: '¡Reordenar!'
invitadxs:
index:
title: Invitadxs
new:
email: Correo electrónico
password: Contraseña
password_confirmation: Repite la contraseña
submit: Registrarme
info:
posts:
reorder: "¡Los artículos fueron reordenados!"

View file

@ -9,9 +9,15 @@ Rails.application.routes.draw do
get '/sites/:site_id/public/:type/:basename', to: 'sites#send_public_file'
resources :invitadxs, only: [:create, :show] do
end
resources :sites, only: [:index, :show], constraints: { site_id: /[^\/]+/, id: /[^\/]+/ } do
resources :posts
resources :templates
resources :invitadxs, only: [:index, :new] do
get :confirmation, to: 'invitadxs#confirmation'
end
get 'i18n', to: 'i18n#index'
get 'i18n/edit', to: 'i18n#edit'

View file

@ -0,0 +1,11 @@
class CreateInvitadxs < ActiveRecord::Migration[5.1]
def change
create_table :invitadxs do |t|
t.timestamps
t.string :password_digest
t.string :email, unique: true, index: true
t.string :confirmation_token
t.boolean :confirmed
end
end
end

53
doc/invitadxs.md Normal file
View file

@ -0,0 +1,53 @@
# Gestión de usuarixs invitadxs
Lxs usuarixs invitadxs solo tienen cuenta en Sutty, no comparten cuenta
con el resto del sistema (Sutty está integrada a otras cuentas vía
IMAP).
Pueden cambiar su contraseña y recuperarla.
Cuando se loguean, solo pueden ver sus artículos y editarlos. También
pueden crear nuevos.
Cuando crean un artículo o cuando lo editan, los artículos pasan a
estado borrador. Solo las usuarias de Sutty pueden revisar el artículo
y publicarlo.
Cada invitadx está asociadx a uno o más sitios.
## Idea
Que cada sitio gestione sus propias cuentas usando un archivo en formato
`/etc/passwd` (ver `man 5 passwd`).
Con esto habría portabilidad de cuentas junto con los sitios, pero sería
un problema para poder gestionar varios sitios con cuentas compartidas.
## Temporal
Usar `devise` con una base de datos SQLite. La idea es descartarla más
adelante y tener un `passwd` + `shadow` de Sutty. Sino vamos a empezar
a poner cosas en una base de datos y no es la idea...
**No se puede usar devise porque toma el control de toda la gestión de
usuarias.**
## Implementación
Para poder separar la autenticación de usuarias de invitadxs, cada
controlador tiene su propio `namespace`, de forma que no se crucen
funcionalidades.
Lxs invitadxs se almacenan con email y contraseña en una base de datos
SQLite3.
La pertenencia a un sitio se almacena en el archivo `.invitadxs` de cada
sitio (como `.usuarias`).
Además, se vincula el sitio al directorio de invitadx para poder tener
acceso a varios sitios.
El directorio de lx invitadx es `_invitadxs/direccion@mail`.
El registro de cuentas se hace en base al sitio.

View file

@ -0,0 +1,32 @@
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'])
if u.try(:authenticate, params['password'])
if u.confirmed?
success! u
else
fail! 'unconfirmed'
end
end
end
end
end
end

0
public/covers/.keep Normal file
View file