diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index bb1e69c..166b19a 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -23,6 +23,9 @@ class ApplicationController < ActionController::Base # lugar de desperdiciar una consulta current_usuarie.sites.find_by_name(id) || current_usuarie.sites_as_invitade.find_by_name(id) + + # TODO: reenviar a un 403 si el sitio ya no está permitido para le + # usuarie end def find_post(site) diff --git a/app/controllers/usuaries_controller.rb b/app/controllers/usuaries_controller.rb new file mode 100644 index 0000000..1aef1b6 --- /dev/null +++ b/app/controllers/usuaries_controller.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +# Controlador de Usuaries +class UsuariesController < ApplicationController + include Pundit + before_action :authenticate_usuarie! + + # Mostrar todes les usuaries e invitades de un sitio + def index + @site = find_site + site_usuarie = SiteUsuarie.new(@site, current_usuarie) + authorize site_usuarie + + @policy = policy(site_usuarie) + end + + # Desasociar une usuarie de un sitio + def destroy + @site = find_site + authorize SiteUsuarie.new(@site, current_usuarie) + + @usuarie = Usuarie.find(params[:id]) + + @usuarie.sites.delete(@site) + + redirect_to site_usuaries_path + end + + # Convertir une usuarie en invitade + def demote + @site = find_site + authorize SiteUsuarie.new(@site, current_usuarie) + + @usuarie = Usuarie.find(params[:usuarie_id]) + + @usuarie.sites.delete(@site) + @site.invitades << @usuarie + + redirect_to site_usuaries_path + end + + # Convertir invitade en usuarie + def promote + @site = find_site + authorize SiteUsuarie.new(@site, current_usuarie) + + @usuarie = Usuarie.find(params[:usuarie_id]) + + @usuarie.sites_as_invitade.delete(@site) + @site.usuaries << @usuarie + + redirect_to site_usuaries_path + end +end diff --git a/app/models/site_usuarie.rb b/app/models/site_usuarie.rb new file mode 100644 index 0000000..f66ce83 --- /dev/null +++ b/app/models/site_usuarie.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# Clase genérica a través de la que podemos obtener la relación entre +# sitio y usuarie +class SiteUsuarie + attr_reader :site, :usuarie + + def initialize(site, usuarie) + @site = site + @usuarie = usuarie + end +end diff --git a/app/models/usuarie.rb b/app/models/usuarie.rb index 5fee295..b0f2e3b 100644 --- a/app/models/usuarie.rb +++ b/app/models/usuarie.rb @@ -9,6 +9,7 @@ class Usuarie < ApplicationRecord validates_uniqueness_of :email has_and_belongs_to_many :sites - has_and_belongs_to_many :sites_as_invitade, class_name: 'Site', - join_table: 'invitades_sites' + has_and_belongs_to_many :sites_as_invitade, + class_name: 'Site', + join_table: 'invitades_sites' end diff --git a/app/policies/post_policy.rb b/app/policies/post_policy.rb index 3924a01..d1bb993 100644 --- a/app/policies/post_policy.rb +++ b/app/policies/post_policy.rb @@ -4,7 +4,7 @@ # # TODO: Implementar Invitadx class PostPolicy - attr_reader :post + attr_reader :post, :usuarie def initialize(usuarie, post) @usuarie = usuarie @@ -17,7 +17,7 @@ class PostPolicy # Lxs invitadxs solo pueden ver sus propios posts def show? - true || post.author == usuarix.email + post.site.usuarie?(usuarie) || post.author == usuarie.email end def new? @@ -34,7 +34,7 @@ class PostPolicy # Lxs invitadxs solo pueden modificar sus propios artículos def update? - true || post.author == usuarix.email + post.site.usuarie?(usuarie) || post.author == usuarie.email end # Solo las usuarias pueden eliminar artículos. Lxs invitadxs pueden @@ -58,8 +58,8 @@ class PostPolicy # # Lxs invitadxs solo pueden ver sus propios posts def resolve - # TODO: filtrar por invitade - return scope + return scope if scope.try(:first).try(:site).try(:usuarie?, usuarie) + # Asegurarse que al menos devolvemos [] [scope.find do |post| post.author == usuarie.email diff --git a/app/policies/site_usuarie_policy.rb b/app/policies/site_usuarie_policy.rb new file mode 100644 index 0000000..d373480 --- /dev/null +++ b/app/policies/site_usuarie_policy.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# Gestiona la relación entre sitios y usuaries +class SiteUsuariePolicy + attr_reader :usuarie, :site_usuarie + + def initialize(usuarie, site_usuarie) + @usuarie = usuarie + @site_usuarie = site_usuarie + end + + def index? + usuarie? + end + + # Les usuaries pueden remover a otres usuaries e invitades del sitio + def destroy? + usuarie? + end + + # Les usuaries pueden convertir a otres usuaries en invitades + def demote? + usuarie? + end + + def promote? + usuarie? + end + + private + + def usuarie? + site_usuarie.site.usuarie? usuarie + end +end diff --git a/app/views/posts/index.haml b/app/views/posts/index.haml index 606b392..0b26662 100644 --- a/app/views/posts/index.haml +++ b/app/views/posts/index.haml @@ -17,7 +17,7 @@ = link_to t('posts.new_with_template', template: @site.templates.first.id.humanize), new_site_post_path(@site, lang: @lang, template: @site.templates.first.id), class: 'btn btn-success' - - if policy(Post).usuaria? + - if @site.usuarie? current_usuarie %button.btn.btn-success.dropdown-toggle.dropdown-toggle-split{data: { toggle: 'split' }, aria: { haspopup: 'true', expanded: 'false' }} %span.sr-only= t('posts.dropdown') diff --git a/app/views/sites/index.haml b/app/views/sites/index.haml index 93cc320..fe67360 100644 --- a/app/views/sites/index.haml +++ b/app/views/sites/index.haml @@ -33,6 +33,12 @@ text: t('i18n.edit'), type: 'info', link: site_i18n_edit_path(site) + - if policy(SiteUsuarie.new(site, current_usuarie)).index? + = render 'layouts/btn_with_tooltip', + tooltip: t('usuaries.index.help.self'), + text: t('usuaries.index.title'), + type: 'info', + link: site_usuaries_path(site) - if policy(site).build? - if site.enqueued? = render 'layouts/btn_with_tooltip', diff --git a/app/views/usuaries/index.haml b/app/views/usuaries/index.haml new file mode 100644 index 0000000..733e434 --- /dev/null +++ b/app/views/usuaries/index.haml @@ -0,0 +1,50 @@ +.row + .col + = render 'layouts/breadcrumb', + crumbs: [ link_to(t('sites.index'), sites_path), + @site.name, + link_to(t('posts.index'), + site_usuaries_path(@site)) ] + = render 'layouts/help', help: t('help.breadcrumbs') +.row + .col + %h1= t('.title') + +.row + .col + -# Una tabla de usuaries y otra de invitades, con acciones + - %i[usuaries invitades].each do |u| + %h2= t(".#{u.to_s}") + %p= t(".help.#{u.to_s}") + %table.table.table-striped.table-condensed + %tbody + - @site.send(u).each do |cuenta| + %tr + -# TODO: avatares + %td= cuenta.email + %td + .btn-group{role: 'group', 'aria-label': t('.actions')} + - if @policy.demote? && @site.usuarie?(cuenta) + = link_to t('.demote.text'), + site_usuarie_demote_path(@site, cuenta), + class: 'btn btn-warning', + data: { toggle: 'tooltip', + confirm: t('.demote.confirm')}, + title: t('.help.demote'), + method: :patch + - if @policy.promote? && @site.invitade?(cuenta) + = link_to t('.promote.text'), + site_usuarie_promote_path(@site, cuenta), + class: 'btn btn-success', + data: { toggle: 'tooltip', + confirm: t('.promote.confirm')}, + title: t('.help.promote'), + method: :patch + - if @policy.destroy? + = link_to t('.destroy.text'), + site_usuarie_path(@site, cuenta), + class: 'btn btn-danger', + data: { toggle: 'tooltip', + confirm: t('.destroy.confirm')}, + title: t('.help.destroy'), + method: :delete diff --git a/config/locales/en.yml b/config/locales/en.yml index 051224b..ae0fdff 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -258,3 +258,24 @@ en: blank: Nothing destroy: Delete confirm_destroy: Are you sure? + usuaries: + index: + help: + self: Self-manage who has access to this site + destroy: Remove access to this site + usuaries: 'Users can self-manage every section and option of this site, posts, review and approve posts from guests, publish changes, etc.' + 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 + title: Users and Guests + usuaries: Users + invitades: Guests + destroy: + text: 'Remove access' + confirm: "Remove access to this site? The account itself is not deleted, but it won't be able to make changes to this site." + demote: + text: 'Convert to guest' + confirm: 'Convert to guest? They can only edit their own posts and will need approval from other user to publish them.' + promote: + text: 'Convert to user' + confirm: 'Convert to user? They will gain full access to self-manage this site.' diff --git a/config/locales/es.yml b/config/locales/es.yml index 82b8392..f34996a 100644 --- a/config/locales/es.yml +++ b/config/locales/es.yml @@ -265,3 +265,24 @@ es: blank: En blanco destroy: Borrar confirm_destroy: ¿Estás segurx? + usuaries: + index: + help: + self: Gestionar quiénes tienen acceso a este sitio + destroy: Impide el acceso a la gestión del sitio + usuaries: 'Les usuaries pueden gestionar todas las secciones del sitio, crear artículos, revisar y aprobar artículos de invitades, publicar los cambios, etc.' + 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 + title: Usuaries e invitades + usuaries: Usuaries + invitades: Invitades + destroy: + text: 'Quitar acceso' + confirm: '¿Quitar acceso a este sitio? La cuenta no será modificada, solo no podrá hacer cambios en este sitio.' + demote: + text: 'Convertir en invitade' + confirm: '¿Convertir en invitade? Solo tendrá acceso a sus propios artículos y necesitará la aprobación de otre usuarie para publicarlos.' + promote: + text: 'Convertir en usuarie' + confirm: '¿Convertir en usuarie? Ganará acceso a la gestión completa del sitio.' diff --git a/config/routes.rb b/config/routes.rb index ce11f6d..3f22545 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -16,16 +16,21 @@ Rails.application.routes.draw 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' + # Gestionar usuaries + resources :usuaries do + patch 'demote', to: 'usuaries#demote' + patch 'promote', to: 'usuaries#promote' end + # Gestionar artículos + resources :posts + + # Gestionar traducciones get 'i18n', to: 'i18n#index' get 'i18n/edit', to: 'i18n#edit' post 'i18n', to: 'i18n#update' + # Compilar el sitio post 'enqueue', to: 'sites#enqueue' get 'build_log', to: 'sites#build_log' post 'reorder_posts', to: 'sites#reorder_posts' diff --git a/db/migrate/20190705195758_add_unique_to_invitades_and_usuaries.rb b/db/migrate/20190705195758_add_unique_to_invitades_and_usuaries.rb new file mode 100644 index 0000000..a2649d1 --- /dev/null +++ b/db/migrate/20190705195758_add_unique_to_invitades_and_usuaries.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# Agrega índices únicos a la combinación de Usuarie y Site para no tener +# accesos duplicados. +class AddUniqueToInvitadesAndUsuaries < ActiveRecord::Migration[5.2] + def change + %i[invitades_sites sites_usuaries].each do |t| + remove_index t, :site_id + remove_index t, :usuarie_id + add_index t, %i[site_id usuarie_id], unique: true + end + end +end diff --git a/db/schema.rb b/db/schema.rb index c7dd837..c1b435f 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -12,12 +12,11 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20_190_703_200_455) do +ActiveRecord::Schema.define(version: 20_190_705_195_758) do create_table 'invitades_sites', id: false, force: :cascade do |t| t.integer 'site_id' t.integer 'usuarie_id' - t.index ['site_id'], name: 'index_invitades_sites_on_site_id' - t.index ['usuarie_id'], name: 'index_invitades_sites_on_usuarie_id' + t.index %w[site_id usuarie_id], name: 'index_invitades_sites_on_site_id_and_usuarie_id', unique: true end create_table 'sites', force: :cascade do |t| @@ -30,8 +29,7 @@ ActiveRecord::Schema.define(version: 20_190_703_200_455) do create_table 'sites_usuaries', id: false, force: :cascade do |t| t.integer 'site_id' t.integer 'usuarie_id' - t.index ['site_id'], name: 'index_sites_usuaries_on_site_id' - t.index ['usuarie_id'], name: 'index_sites_usuaries_on_usuarie_id' + t.index %w[site_id usuarie_id], name: 'index_sites_usuaries_on_site_id_and_usuarie_id', unique: true end create_table 'usuaries', force: :cascade do |t| diff --git a/doc/autenticacion.md b/doc/autenticacion.md index b73ff02..c5b3813 100644 --- a/doc/autenticacion.md +++ b/doc/autenticacion.md @@ -36,4 +36,10 @@ De lo contrario necesitamos establecer roles y ya entramos en las dificultades que decíamos más arriba. No podemos saber desde cuándo se creo la relación, a menos que tengamos -una tabla de actividades. +una tabla de actividades por separado. + +Podemos saber quién es invitade ingresando a un sitio y fijándonos si +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).