mirror of
https://0xacab.org/sutty/sutty
synced 2024-11-21 22:16:22 +00:00
gestionar usuaries de un sitio
This commit is contained in:
parent
a125b388bb
commit
64fcf61c67
22 changed files with 348 additions and 5 deletions
|
@ -19,6 +19,7 @@ Metrics/AbcSize:
|
|||
- 'app/controllers/posts_controller.rb'
|
||||
- 'app/controllers/invitadxs_controller.rb'
|
||||
- 'app/controllers/i18n_controller.rb'
|
||||
- 'app/controllers/usuaries_controller.rb'
|
||||
|
||||
Metrics/MethodLength:
|
||||
Exclude:
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -41,6 +41,7 @@ gem 'carrierwave-i18n'
|
|||
gem 'commonmarker'
|
||||
gem 'devise'
|
||||
gem 'devise-i18n'
|
||||
gem 'devise_invitable'
|
||||
gem 'email_address'
|
||||
gem 'exception_notification'
|
||||
gem 'font-awesome-rails'
|
||||
|
|
|
@ -106,6 +106,9 @@ GEM
|
|||
warden (~> 1.2.3)
|
||||
devise-i18n (1.8.0)
|
||||
devise (>= 4.6)
|
||||
devise_invitable (2.0.1)
|
||||
actionmailer (>= 5.0)
|
||||
devise (>= 4.6)
|
||||
dotenv (2.7.2)
|
||||
dotenv-rails (2.7.2)
|
||||
dotenv (= 2.7.2)
|
||||
|
@ -363,6 +366,7 @@ DEPENDENCIES
|
|||
commonmarker
|
||||
devise
|
||||
devise-i18n
|
||||
devise_invitable
|
||||
dotenv-rails
|
||||
ed25519
|
||||
email_address
|
||||
|
|
|
@ -51,4 +51,59 @@ class UsuariesController < ApplicationController
|
|||
|
||||
redirect_to site_usuaries_path
|
||||
end
|
||||
|
||||
# Poder invitar
|
||||
def invite
|
||||
@site = find_site
|
||||
site_usuarie = SiteUsuarie.new(@site, current_usuarie)
|
||||
authorize site_usuarie
|
||||
|
||||
@policy = policy(site_usuarie)
|
||||
end
|
||||
|
||||
# Envía las invitaciones
|
||||
def send_invitations
|
||||
@site = find_site
|
||||
authorize SiteUsuarie.new(@site, current_usuarie)
|
||||
|
||||
# Enviar la invitación si es necesario y agregar al sitio
|
||||
#
|
||||
# TODO: Pedir consentimiento para agregar a un sitio!
|
||||
invitaciones.each do |invitacion|
|
||||
# Si la cuenta no existe, envía una invitación por correo
|
||||
usuarie = Usuarie.invite! email: invitacion.address
|
||||
|
||||
# No invitar al sitio si ya estaba en la lista!
|
||||
unless @site.invitade?(usuarie) || @site.usuarie?(usuarie)
|
||||
@site.send(invited_as) << usuarie
|
||||
end
|
||||
end
|
||||
|
||||
redirect_to site_usuaries_path(@site)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Traer todas las invitaciones que al menos tengan usuarie y dominio
|
||||
def invitaciones
|
||||
# XXX: Podríamos usar EmailAddress pero hace chequeos más lentos
|
||||
i = params[:invitaciones].split("\n").map do |m|
|
||||
Mail::Address.new m
|
||||
rescue Mail::Field::IncompleteParseError
|
||||
nil
|
||||
end.compact
|
||||
|
||||
i.select do |m|
|
||||
m.local && m.domain
|
||||
end
|
||||
end
|
||||
|
||||
# El tipo de invitación que tenemos que enviar, si alguien mandó
|
||||
# cualquier cosa, usamos el privilegio menor.
|
||||
def invited_as
|
||||
invited_as = params[:invited_as].try(:to_sym)
|
||||
invited_as = :invitades unless %i[usuaries invitades].include?(invited_as)
|
||||
|
||||
invited_as
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
# Usuarie de la plataforma
|
||||
class Usuarie < ApplicationRecord
|
||||
devise :database_authenticatable,
|
||||
devise :invitable, :database_authenticatable,
|
||||
:recoverable, :rememberable, :validatable,
|
||||
:confirmable, :lockable, :registerable
|
||||
|
||||
|
|
|
@ -27,6 +27,14 @@ class SiteUsuariePolicy
|
|||
usuarie?
|
||||
end
|
||||
|
||||
def invite?
|
||||
usuarie?
|
||||
end
|
||||
|
||||
def send_invitations?
|
||||
usuarie?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def usuarie?
|
||||
|
|
16
app/views/devise/invitations/edit.haml
Normal file
16
app/views/devise/invitations/edit.haml
Normal file
|
@ -0,0 +1,16 @@
|
|||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-6.align-self-center
|
||||
%h2= t 'devise.invitations.edit.header'
|
||||
= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :put }) do |f|
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
= f.hidden_field :invitation_token, readonly: true
|
||||
- if f.object.class.require_password_on_accepting
|
||||
.form-group
|
||||
= f.label :password
|
||||
= f.password_field :password, class: 'form-control'
|
||||
.form-group
|
||||
= f.label :password_confirmation
|
||||
= f.password_field :password_confirmation, class: 'form-control'
|
||||
.actions
|
||||
= f.submit t('devise.invitations.edit.submit_button'),
|
||||
class: 'btn btn-lg btn-primary btn-block'
|
12
app/views/devise/invitations/new.haml
Normal file
12
app/views/devise/invitations/new.haml
Normal file
|
@ -0,0 +1,12 @@
|
|||
.row.align-items-center.justify-content-center.full-height
|
||||
.col-md-6.align-self-center
|
||||
%h2= t 'devise.invitations.new.header'
|
||||
= form_for(resource, as: resource_name, url: invitation_path(resource_name), html: { method: :post }) do |f|
|
||||
= render 'devise/shared/error_messages', resource: resource
|
||||
- resource.class.invite_key_fields.each do |field|
|
||||
.form-group
|
||||
= f.label field
|
||||
= f.text_field field, class: 'form-control'
|
||||
.actions
|
||||
= f.submit t('devise.invitations.new.submit_button'),
|
||||
class: 'btn btn-lg btn-primary btn-block'
|
6
app/views/devise/mailer/invitation_instructions.haml
Normal file
6
app/views/devise/mailer/invitation_instructions.haml
Normal file
|
@ -0,0 +1,6 @@
|
|||
%p= t("devise.mailer.invitation_instructions.hello", email: @resource.email)
|
||||
%p= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url)
|
||||
%p= link_to t("devise.mailer.invitation_instructions.accept"), accept_invitation_url(@resource, invitation_token: @token)
|
||||
- if @resource.invitation_due_at
|
||||
%p= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format'))
|
||||
%p= t("devise.mailer.invitation_instructions.ignore")
|
|
@ -0,0 +1,6 @@
|
|||
= t("devise.mailer.invitation_instructions.hello", email: @resource.email)
|
||||
= t("devise.mailer.invitation_instructions.someone_invited_you", url: root_url)
|
||||
= accept_invitation_url(@resource, invitation_token: @token)
|
||||
- if @resource.invitation_due_at
|
||||
= t("devise.mailer.invitation_instructions.accept_until", due_date: l(@resource.invitation_due_at, format: :'devise.mailer.invitation_instructions.accept_until_format'))
|
||||
= t("devise.mailer.invitation_instructions.ignore")
|
|
@ -1 +0,0 @@
|
|||
<%= yield %>
|
|
@ -14,7 +14,15 @@
|
|||
.col
|
||||
-# Una tabla de usuaries y otra de invitades, con acciones
|
||||
- %i[usuaries invitades].each do |u|
|
||||
%h2= t(".#{u.to_s}")
|
||||
%h2
|
||||
= t(".#{u.to_s}")
|
||||
.btn-group{role: 'group', 'aria-label': t('.actions')}
|
||||
- if @policy.invite?
|
||||
= link_to t('.invite.text'),
|
||||
site_usuaries_invite_path(@site, invite_as: u.to_s),
|
||||
class: 'btn btn-success',
|
||||
data: { toggle: 'tooltip' },
|
||||
title: t('.help.invite', invite_as: u.to_s)
|
||||
%p= t(".help.#{u.to_s}")
|
||||
%table.table.table-striped.table-condensed
|
||||
%tbody
|
||||
|
@ -23,7 +31,7 @@
|
|||
-# TODO: avatares
|
||||
%td= cuenta.email
|
||||
%td
|
||||
.btn-group{role: 'group', 'aria-label': t('.actions')}
|
||||
.btn-group{role: 'group', 'aria-label': t('.individual_actions')}
|
||||
- if @policy.demote? && @site.usuarie?(cuenta)
|
||||
= link_to t('.demote.text'),
|
||||
site_usuarie_demote_path(@site, cuenta),
|
||||
|
|
26
app/views/usuaries/invite.haml
Normal file
26
app/views/usuaries/invite.haml
Normal file
|
@ -0,0 +1,26 @@
|
|||
- invite_as = t("usuaries.invite_as.#{params[:invite_as]}")
|
||||
.row
|
||||
.col
|
||||
= render 'layouts/breadcrumb',
|
||||
crumbs: [ link_to(t('sites.index'), sites_path),
|
||||
@site.name,
|
||||
link_to(t('posts.index'),
|
||||
site_usuaries_path(@site)),
|
||||
t('.title', invite_as: invite_as) ]
|
||||
= render 'layouts/help', help: t('help.breadcrumbs')
|
||||
.row
|
||||
.col
|
||||
%h1= t('.title', invite_as: invite_as)
|
||||
|
||||
.row
|
||||
.col
|
||||
= form_with url: site_usuaries_invite_path(@site) do |f|
|
||||
= f.hidden_field :invite_as, value: params[:invite_as]
|
||||
.form-group
|
||||
= f.label :invitaciones do
|
||||
= t('.invitaciones')
|
||||
%small.text-muted.form-text= t('.help.invitaciones',
|
||||
invite_as: invite_as)
|
||||
= f.text_area :invitaciones, class: 'form-control'
|
||||
.form-group
|
||||
= f.submit t('.submit'), class: 'btn btn-success'
|
|
@ -136,6 +136,58 @@ Devise.setup do |config|
|
|||
# Send a notification email when the user's password is changed.
|
||||
config.send_password_change_notification = true
|
||||
|
||||
# ==> Configuration for :invitable
|
||||
# The period the generated invitation token is valid. After this
|
||||
# period, the invited resource won't be able to accept the invitation.
|
||||
# When invite_for is 0 (the default), the invitation won't expire.
|
||||
config.invite_for = 2.weeks
|
||||
|
||||
# Number of invitations users can send.
|
||||
# - If invitation_limit is nil, there is no limit for invitations,
|
||||
# users can send unlimited invitations, invitation_limit column is not
|
||||
# used.
|
||||
# - If invitation_limit is 0, users can't send invitations by default.
|
||||
# - If invitation_limit n > 0, users can send n invitations.
|
||||
# You can change invitation_limit column for some users so they can
|
||||
# send more or less invitations, even with global invitation_limit = 0
|
||||
# Default: nil
|
||||
config.invitation_limit = 0
|
||||
|
||||
# The key to be used to check existing users when sending an
|
||||
# invitation and the regexp used to test it when validate_on_invite is
|
||||
# not set.
|
||||
# config.invite_key = { email: /\A[^@]+@[^@]+\z/ }
|
||||
# config.invite_key = { email: /\A[^@]+@[^@]+\z/, username: nil }
|
||||
|
||||
# Ensure that invited record is valid.
|
||||
# The invitation won't be sent if this check fails.
|
||||
# Default: false
|
||||
# config.validate_on_invite = true
|
||||
|
||||
# Resend invitation if user with invited status is invited again
|
||||
# Default: true
|
||||
# config.resend_invitation = false
|
||||
|
||||
# The class name of the inviting model. If this is nil,
|
||||
# the #invited_by association is declared to be polymorphic.
|
||||
# Default: nil
|
||||
# config.invited_by_class_name = 'User'
|
||||
|
||||
# The foreign key to the inviting model (if invited_by_class_name is
|
||||
# set)
|
||||
# Default: :invited_by_id
|
||||
# config.invited_by_foreign_key = :invited_by_id
|
||||
|
||||
# The column name used for counter_cache column. If this is nil,
|
||||
# the #invited_by association is declared without counter_cache.
|
||||
# Default: nil
|
||||
# config.invited_by_counter_cache = :invitations_count
|
||||
|
||||
# Auto-login after the user accepts the invite. If this is false,
|
||||
# the user will need to manually log in after accepting the invite.
|
||||
# Default: true
|
||||
# config.allow_insecure_sign_in_after_accept = false
|
||||
|
||||
# ==> Configuration for :confirmable
|
||||
# A period that the user is allowed to access the website even without
|
||||
# confirming their account. For instance, if set to 2.days, the user
|
||||
|
|
31
config/locales/devise_invitable.en.yml
Normal file
31
config/locales/devise_invitable.en.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
en:
|
||||
devise:
|
||||
failure:
|
||||
invited: "You have a pending invitation, accept it to finish creating your account."
|
||||
invitations:
|
||||
send_instructions: "An invitation email has been sent to %{email}."
|
||||
invitation_token_invalid: "The invitation token provided is not valid!"
|
||||
updated: "Your password was set successfully. You are now signed in."
|
||||
updated_not_active: "Your password was set successfully."
|
||||
no_invitations_remaining: "No invitations remaining"
|
||||
invitation_removed: "Your invitation was removed."
|
||||
new:
|
||||
header: "Send invitation"
|
||||
submit_button: "Send an invitation"
|
||||
edit:
|
||||
header: "Set your password"
|
||||
submit_button: "Set my password"
|
||||
mailer:
|
||||
invitation_instructions:
|
||||
subject: "Invitation instructions"
|
||||
hello: "Hello %{email}"
|
||||
someone_invited_you: "Someone has invited you to %{url}, you can accept it through the link below."
|
||||
accept: "Accept invitation"
|
||||
accept_until: "This invitation will be due in %{due_date}."
|
||||
ignore: "If you don't want to accept the invitation, please ignore this email. Your account won't be created until you access the link above and set your password."
|
||||
time:
|
||||
formats:
|
||||
devise:
|
||||
mailer:
|
||||
invitation_instructions:
|
||||
accept_until_format: "%B %d, %Y %I:%M %p"
|
31
config/locales/devise_invitable.es.yml
Normal file
31
config/locales/devise_invitable.es.yml
Normal file
|
@ -0,0 +1,31 @@
|
|||
es:
|
||||
devise:
|
||||
failure:
|
||||
invited: "Tenés una invitación pendiente, aceptala para terminar de crear tu cuenta."
|
||||
invitations:
|
||||
send_instructions: "Hemos enviado una invitación a %{email}."
|
||||
invitation_token_invalid: "¡El código de invitación no es válido!"
|
||||
updated: "Configuraste tu contraseña correctamente, a partir de ahora podés ingresar."
|
||||
updated_not_active: "Configuraste tu contraseña correctamente."
|
||||
no_invitations_remaining: "No te quedan invitaciones"
|
||||
invitation_removed: "La invitación ha sido eliminada."
|
||||
new:
|
||||
header: "Invitar a alguien"
|
||||
submit_button: "Enviar la invitación"
|
||||
edit:
|
||||
header: "Configura tu contraseña"
|
||||
submit_button: "Configurar mi contraseña"
|
||||
mailer:
|
||||
invitation_instructions:
|
||||
subject: "Invitación"
|
||||
hello: "Hola %{email}"
|
||||
someone_invited_you: "Alguien te ha invitado a colaborar en %{url}, podés aceptar la invitación con el enlace a continuación."
|
||||
accept: "Aceptar la invitación"
|
||||
accept_until: "La invitación vencerá en %{due_date}."
|
||||
ignore: "Si no querés aceptar la invitación, por favor ignora este correo. Tu cuenta no será creada hasta que aceptes la invitación y configures una contraseña."
|
||||
time:
|
||||
formats:
|
||||
devise:
|
||||
mailer:
|
||||
invitation_instructions:
|
||||
accept_until_format: "%d/%m/%Y a las %H:%M"
|
|
@ -259,6 +259,9 @@ en:
|
|||
destroy: Delete
|
||||
confirm_destroy: Are you sure?
|
||||
usuaries:
|
||||
invite_as:
|
||||
usuaries: users
|
||||
invitades: guests
|
||||
index:
|
||||
help:
|
||||
self: Self-manage who has access to this site
|
||||
|
@ -267,6 +270,7 @@ en:
|
|||
invitades: 'Guests can only create new posts and edit those authored by them. Any change needs review and approval by a user.'
|
||||
demote: Removes privileges for this user
|
||||
promote: Gives privileges to this guest
|
||||
invite: 'Invites %{invite_as} to this site'
|
||||
title: Users and Guests
|
||||
usuaries: Users
|
||||
invitades: Guests
|
||||
|
@ -279,3 +283,11 @@ en:
|
|||
promote:
|
||||
text: 'Convert to user'
|
||||
confirm: 'Convert to user? They will gain full access to self-manage this site.'
|
||||
invite:
|
||||
text: 'Invite'
|
||||
invite:
|
||||
title: 'Invite %{invite_as}'
|
||||
help:
|
||||
invitaciones: 'You can invite new %{invite_as} by entering an email address per line. They will receive an email with your invitation and decide if they want to accept or not.'
|
||||
invitaciones: Send invitations to these addresses
|
||||
submit: Send invitations
|
||||
|
|
|
@ -266,6 +266,9 @@ es:
|
|||
destroy: Borrar
|
||||
confirm_destroy: ¿Estás segurx?
|
||||
usuaries:
|
||||
invite_as:
|
||||
usuaries: usuaries
|
||||
invitades: invitades
|
||||
index:
|
||||
help:
|
||||
self: Gestionar quiénes tienen acceso a este sitio
|
||||
|
@ -274,6 +277,7 @@ es:
|
|||
invitades: 'Les invitades sólo pueden crear artículos nuevos y modificar los que cargaron. Todos los cambios que hagan necesitan la revisión y aprobación de une usuarie.'
|
||||
demote: Quita privilegios a este usuarie
|
||||
promote: Otorga privilegios a este invitade
|
||||
invite: 'Invita %{invite_as} a colaborar en este sitio'
|
||||
title: Usuaries e invitades
|
||||
usuaries: Usuaries
|
||||
invitades: Invitades
|
||||
|
@ -286,3 +290,11 @@ es:
|
|||
promote:
|
||||
text: 'Convertir en usuarie'
|
||||
confirm: '¿Convertir en usuarie? Ganará acceso a la gestión completa del sitio.'
|
||||
invite:
|
||||
text: 'Invitar'
|
||||
invite:
|
||||
title: 'Invitar %{invite_as}'
|
||||
help:
|
||||
invitaciones: 'Invita nueves %{invite_as} ingresando una dirección de correo electrónico por línea. Recibirán un correo con tu invitación y podrán aceptarla o no.'
|
||||
invitaciones: Invitar a estas direcciones
|
||||
submit: Enviar invitaciones
|
||||
|
|
|
@ -17,6 +17,8 @@ Rails.application.routes.draw do
|
|||
get 'public/:type/:basename', to: 'sites#send_public_file'
|
||||
|
||||
# Gestionar usuaries
|
||||
get 'usuaries/invite', to: 'usuaries#invite'
|
||||
post 'usuaries/invite', to: 'usuaries#send_invitations'
|
||||
resources :usuaries do
|
||||
patch 'demote', to: 'usuaries#demote'
|
||||
patch 'promote', to: 'usuaries#promote'
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# Agregar gestión de invitaciones a usuaries
|
||||
class DeviseInvitableAddToUsuaries < ActiveRecord::Migration[5.2]
|
||||
def up
|
||||
change_table :usuaries do |t|
|
||||
t.string :invitation_token
|
||||
t.datetime :invitation_created_at
|
||||
t.datetime :invitation_sent_at
|
||||
t.datetime :invitation_accepted_at
|
||||
t.integer :invitation_limit
|
||||
t.references :invited_by, polymorphic: true
|
||||
t.integer :invitations_count, default: 0
|
||||
t.index :invitations_count
|
||||
t.index :invitation_token, unique: true # for invitable
|
||||
t.index :invited_by_id
|
||||
end
|
||||
end
|
||||
|
||||
def down
|
||||
change_table :usuaries do |t|
|
||||
t.remove_references :invited_by, polymorphic: true
|
||||
t.remove :invitations_count, :invitation_limit, :invitation_sent_at, :invitation_accepted_at, :invitation_token, :invitation_created_at
|
||||
end
|
||||
end
|
||||
end
|
14
db/schema.rb
14
db/schema.rb
|
@ -12,7 +12,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema.define(version: 20_190_705_195_758) do
|
||||
ActiveRecord::Schema.define(version: 20_190_705_215_536) do
|
||||
create_table 'invitades_sites', id: false, force: :cascade do |t|
|
||||
t.integer 'site_id'
|
||||
t.integer 'usuarie_id'
|
||||
|
@ -48,8 +48,20 @@ ActiveRecord::Schema.define(version: 20_190_705_195_758) do
|
|||
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.integer 'invited_by_id'
|
||||
t.integer 'invitations_count', default: 0
|
||||
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 %w[invited_by_type invited_by_id], name: 'index_usuaries_on_invited_by_type_and_invited_by_id'
|
||||
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
|
||||
|
|
|
@ -43,3 +43,26 @@ está en su lista de invitade. Lo mismo para usuaries.
|
|||
|
||||
Les usuaries pueden bloquear invitades y a otres usuaries, y sumar
|
||||
usuaries e invitades a su sitio (via correo de invitación).
|
||||
|
||||
## Invitaciones a sitios
|
||||
|
||||
### Enviar invitación
|
||||
|
||||
Desde la gestión del sitio se puede invitar a nueves usuaries e
|
||||
invitades. Se les envía un correo (y cuando tengamos sistema de
|
||||
notificaciones, una notificación) donde pueden confirmar su
|
||||
participación.
|
||||
|
||||
Si no tienen cuenta, tienen que registrarse completando los datos en el
|
||||
momento, sino se pueden loguear normalmente.
|
||||
|
||||
Si ya están logueades, se acepta la invitación inmediatamente.
|
||||
|
||||
### Invitades desde la web
|
||||
|
||||
Al publicar la URL de invitación, les invitades se puedan registrar
|
||||
por su cuenta. Solo deben completar sus datos (correo y contraseña) y
|
||||
reciben un correo de confirmación.
|
||||
|
||||
En la configuración del sitio hay que distinguir entre invitades por
|
||||
invitación o por registro automático.
|
||||
|
|
Loading…
Reference in a new issue