mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-14 17:21:43 +00:00
wip de invitadxs
This commit is contained in:
parent
2e4219ce07
commit
39b575ebfb
23 changed files with 368 additions and 19 deletions
2
Gemfile
2
Gemfile
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
53
app/controllers/invitadxs_controller.rb
Normal file
53
app/controllers/invitadxs_controller.rb
Normal 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
|
7
app/mailers/invitadx_mailer.rb
Normal file
7
app/mailers/invitadx_mailer.rb
Normal 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
76
app/models/invitadx.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
%h1= t('.hi')
|
||||
|
||||
%p= t('.body')
|
||||
|
||||
%code= site_invitadx_confirmation_url(@site, @invitadx, confirmation_token: @invitadx.confirmation_token)
|
5
app/views/invitadx_mailer/confirmation_required.txt.haml
Normal file
5
app/views/invitadx_mailer/confirmation_required.txt.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
= t('.hi')
|
||||
|
||||
= t('.body')
|
||||
|
||||
= site_invitadx_confirmation_url(@site, @invitadx, confirmation_token: @invitadx.confirmation_token)
|
13
app/views/invitadxs/index.haml
Normal file
13
app/views/invitadxs/index.haml
Normal 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
|
31
app/views/invitadxs/new.haml
Normal file
31
app/views/invitadxs/new.haml
Normal 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} ×
|
||||
- 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} ×
|
||||
|
||||
= 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};" : ''
|
3
app/views/invitadxs/show.haml
Normal file
3
app/views/invitadxs/show.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-6.align-self-center
|
||||
= t('.confirmation_sent')
|
|
@ -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
|
||||
|
|
3
app/views/layouts/mailer.html.haml
Normal file
3
app/views/layouts/mailer.html.haml
Normal file
|
@ -0,0 +1,3 @@
|
|||
%html
|
||||
%body
|
||||
= yield
|
1
app/views/layouts/mailer.text.haml
Normal file
1
app/views/layouts/mailer.text.haml
Normal file
|
@ -0,0 +1 @@
|
|||
= yield
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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!"
|
||||
|
|
|
@ -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!"
|
||||
|
|
|
@ -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'
|
||||
|
|
11
db/migrate/20180925183241_create_invitadxs.rb
Normal file
11
db/migrate/20180925183241_create_invitadxs.rb
Normal 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
53
doc/invitadxs.md
Normal 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.
|
32
lib/warden/email_and_password.rb
Normal file
32
lib/warden/email_and_password.rb
Normal 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
0
public/covers/.keep
Normal file
Loading…
Reference in a new issue