Skip to content

Commit

Permalink
Allow admins to manually verify users (#341)
Browse files Browse the repository at this point in the history
* add system forms

* add commands specs

* fix command

* deface participants page

* add modal

* render authorization modal

* authorize/unauthorize

* handle conflicts and overrides

* add authorization & helper specs

* add permissions and action log entries

* add controller specs

* add adminlog specs

* fix ffi

* add system specs

* add reason to forcer verification

* readme

* readme

* fix specs

* fix locales

* fix config form

* optimize styles spec

* add locales to allowed controllers

* fix spec
  • Loading branch information
microstudi authored Nov 6, 2024
1 parent 7c19c96 commit 2d63fe5
Show file tree
Hide file tree
Showing 60 changed files with 1,816 additions and 205 deletions.
194 changes: 99 additions & 95 deletions Gemfile.lock

Large diffs are not rendered by default.

15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,21 @@ Decidim::DecidimAwesome.configure do |config|
config.force_authorization_allowed_controller_names = %w(account pages homepage)
```

#### 21. Manual verifications

The admin will be allowed to manually authorize users using the methods specified in the `/system` admin section.
Currently, only form based handlers are supported (Direct methods).
Admins can manually override or verify users in the participants list but they still have to fulfill the requirements of the verifier (although they will be allowed to force the authorization even if some of them fails).

Admin logs are also created in each action for accountability.

System configuration:

![System manual authorization config](examples/manual_verifications_system.png)
![List of authorizations](examples/manual_verifications_1.png)
![Removing an authorization](examples/manual_verifications_2.png)
![Creating an authorization](examples/manual_verifications_3.png)

#### To be continued...

We're not done! Please check the [issues](/decidim-ice/decidim-module-decidim_awesome/issues) (and participate) to see what's on our mind
Expand Down
4 changes: 0 additions & 4 deletions app/cells/decidim/decidim_awesome/content_blocks/map_cell.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,7 @@ def global_map_components
true
when :proposals
component.settings.geocoding_enabled
else
false
end
else
false
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# frozen_string_literal: true

module Decidim
module DecidimAwesome
module System
module RegisterOrganizationOverride
extend ActiveSupport::Concern

included do
private

alias_method :decidim_create_organization, :create_organization

def create_organization
@organization = decidim_create_organization
if form.clean_awesome_admins_available_authorizations.present?
AwesomeConfig.create!(
var: :admins_available_authorizations,
organization: @organization,
value: form.clean_awesome_admins_available_authorizations
)
end
@organization
end
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

module Decidim
module DecidimAwesome
module System
module UpdateOrganizationOverride
extend ActiveSupport::Concern

included do
private

alias_method :decidim_original_save_organization, :save_organization

def save_organization
decidim_original_save_organization
if form.clean_awesome_admins_available_authorizations.present?
add_awesome_configs!
elsif awesome_config&.persisted?
awesome_config.destroy!
end
end

def add_awesome_configs!
awesome_config.value = form.clean_awesome_admins_available_authorizations
awesome_config.save!
end

def awesome_config
@awesome_config ||= AwesomeConfig.find_or_initialize_by(var: :admins_available_authorizations, organization: @organization)
end
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def current_authorizations
end

def allowed_controllers
%w(required_authorizations authorizations upload_validations timeouts editor_images) + awesome_config[:force_authorization_allowed_controller_names].to_a
%w(required_authorizations authorizations upload_validations timeouts editor_images locales) + awesome_config[:force_authorization_allowed_controller_names].to_a
end
end
end
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
# frozen_string_literal: true

module Decidim
module DecidimAwesome
module Admin
class AdminAuthorizationsController < DecidimAwesome::Admin::ApplicationController
include NeedsAwesomeConfig

layout false
helper_method :user, :authorization, :workflow, :handler, :conflict
# overwrite original rescue_from to ensure we print messages from ajax methods
rescue_from Decidim::ActionForbidden, with: :json_error

before_action do
enforce_permission_to :edit_config, :admins_available_authorizations, handler: workflow.name
end

def edit
render "authorization" if authorization
end

def update
if conflict
message = render_to_string("conflict")
else
message = render_to_string(partial: "callout", locals: { i18n_key: "user_authorized", klass: "success" })
Decidim::Verifications::AuthorizeUser.call(handler, current_organization) do
on(:transferred) do |transfer|
message += render_to_string(partial: "callout", locals: { i18n_key: "authorization_transferred", klass: "success" }) if transfer.records.any?
end
on(:invalid) do
if force_verification.present?
create_forced_authorization
else
message = render_to_string(partial: "callout", locals: { i18n_key: "user_not_authorized", klass: "alert" })
message += render_to_string("edit", locals: { with_override: true })
end
end
on(:ok) do
Decidim::ActionLogger.log("admin_creates_authorization", current_user, user, nil, user_id: user.id, handler: workflow.name, handler_name: workflow.fullname)
end
end
end

render json: {
message:,
granted: granted?,
userId: user.id,
handler: workflow.name
}
end

def destroy
message = if destroy_authorization
render_to_string(partial: "callout", locals: { i18n_key: "authorization_destroyed", klass: "success" })
else
render_to_string(partial: "callout", locals: { i18n_key: "authorization_not_destroyed", klass: "alert" })
end

render json: {
message:,
granted: granted?,
userId: user.id,
handler: workflow.name
}
end

private

def create_forced_authorization
Decidim::Authorization.create_or_update_from(handler)
Decidim::ActionLogger.log("admin_forces_authorization", current_user, user, nil, handler: workflow.name, user_id: user.id, handler_name: workflow.fullname,
reason: force_verification)
end

def destroy_authorization
if authorization&.destroy
Decidim::ActionLogger.log("admin_destroys_authorization", current_user, user, nil, user_id: user.id, handler: workflow.name, handler_name: workflow.fullname)
end
end

def json_error(exception)
render json: render_to_string(partial: "callout", locals: { message: exception.message, klass: "alert" }), status: :unprocessable_entity
end

def user
@user ||= Decidim::User.find(params[:id])
end

def authorization
@authorization ||= Decidim::Authorization.where.not(granted_at: nil).find_by(user:, name: workflow.name)
end

def granted?
authorization&.reload.present?
rescue ActiveRecord::RecordNotFound
false
end

def workflow
@workflow ||= Decidim::Verifications.find_workflow_manifest(params[:handler])
end

def handler
@handler ||= Decidim::AuthorizationHandler.handler_for(params[:handler], handler_params)
end

def conflict
@conflict ||= Decidim::Authorization.find_by(unique_id: handler.unique_id)
end

def handler_params
(params[:authorization_handler] || {}).merge(user:)
end

def force_verification
@force_verification ||= params[:force_verification].to_s.strip.presence
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Decidim
module DecidimAwesome
module System
module OrganizationFormOverride
extend ActiveSupport::Concern

included do
alias_method :decidim_original_map_model, :map_model

attribute :awesome_admins_available_authorizations, Array[String]

def map_model(model)
decidim_original_map_model(model)
map_awesome_configs(model)
end

def clean_awesome_admins_available_authorizations
return unless awesome_admins_available_authorizations

awesome_admins_available_authorizations.select(&:present?)
end

private

def map_awesome_configs(organization)
self.awesome_admins_available_authorizations = Decidim::DecidimAwesome::AwesomeConfig.find_by(var: :admins_available_authorizations, organization:)&.value
end
end
end
end
end
end
15 changes: 12 additions & 3 deletions app/forms/decidim/decidim_awesome/admin/config_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,7 @@ class ConfigForm < Decidim::Form
validates :validate_body_min_length, presence: true, numericality: { greater_than_or_equal_to: 0 }
validates :validate_body_max_caps_percent, presence: true, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }
validates :validate_body_max_marks_together, presence: true, numericality: { greater_than_or_equal_to: 1 }
validates :force_authorization_after_login, inclusion: { in: lambda { |form|
form.current_organization.available_authorizations & Decidim.authorization_workflows.map(&:name)
} }
validate :force_authorization_after_login_is_valid
# TODO: validate non general admins are here

def self.from_params(params, additional_params = {})
Expand Down Expand Up @@ -136,6 +134,17 @@ def sanitize_labels!
code
end
end

private

def force_authorization_after_login_is_valid
return if force_authorization_after_login.blank?

invalid = force_authorization_after_login - (current_organization.available_authorizations & Decidim.authorization_workflows.map(&:name))
return if invalid.empty?

errors.add(:force_authorization_after_login, :invalid)
end
end
end
end
Expand Down
54 changes: 28 additions & 26 deletions app/helpers/decidim/decidim_awesome/map_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,34 @@ def awesome_map_for(components, &)
end

html_options = {
"class" => "awesome-map",
"id" => "awesome-map",
"data-components" => components.map do |component|
{
id: component.id,
type: component.manifest.name,
name: translated_attribute(component.name),
url: Decidim::EngineRouter.main_proxy(component).root_path,
amendments: component.manifest.name == :proposals ? Decidim::Proposals::Proposal.where(component:).only_emendations.count : 0
}
end.to_json,
"data-hide-controls" => settings_source.try(:hide_controls),
"data-collapsed" => global_settings.collapse,
"data-truncate" => global_settings.truncate || 255,
"data-map-center" => global_settings.map_center,
"data-map-zoom" => global_settings.map_zoom || 8,
"data-menu-merge-components" => global_settings.menu_merge_components,
"data-menu-amendments" => global_settings.menu_amendments,
"data-menu-meetings" => global_settings.menu_meetings,
"data-menu-categories" => global_settings.menu_categories,
"data-menu-hashtags" => global_settings.menu_hashtags,
"data-show-not-answered" => step_settings&.show_not_answered,
"data-show-accepted" => step_settings&.show_accepted,
"data-show-withdrawn" => step_settings&.show_withdrawn,
"data-show-evaluating" => step_settings&.show_evaluating,
"data-show-rejected" => step_settings&.show_rejected
class: "awesome-map",
id: "awesome-map",
data: {
"components" => components.map do |component|
{
id: component.id,
type: component.manifest.name,
name: translated_attribute(component.name),
url: Decidim::EngineRouter.main_proxy(component).root_path,
amendments: component.manifest.name == :proposals ? Decidim::Proposals::Proposal.where(component:).only_emendations.count : 0
}
end.to_json,
"hide-controls" => settings_source.try(:hide_controls),
"collapsed" => global_settings.collapse,
"truncate" => global_settings.truncate || 255,
"map-center" => global_settings.map_center,
"map-zoom" => global_settings.map_zoom || 8,
"menu-merge-components" => global_settings.menu_merge_components,
"menu-amendments" => global_settings.menu_amendments,
"menu-meetings" => global_settings.menu_meetings,
"menu-categories" => global_settings.menu_categories,
"menu-hashtags" => global_settings.menu_hashtags,
"show-not-answered" => step_settings&.show_not_answered,
"show-accepted" => step_settings&.show_accepted,
"show-withdrawn" => step_settings&.show_withdrawn,
"show-evaluating" => step_settings&.show_evaluating,
"show-rejected" => step_settings&.show_rejected
}
}

content_tag(:div, html_options) do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<!-- insert_after "erb[loud]:contains('show_email_modal')" -->

<%= render "decidim/decidim_awesome/admin/officializations/verification_modal" %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- insert_before "td.table-list__actions" -->

<% if awesome_config[:admins_available_authorizations] %>
<%= render "decidim/decidim_awesome/admin/officializations/participants_td", user: %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<!-- insert_before "th:last" -->

<% if awesome_config[:admins_available_authorizations] %>
<%= render "decidim/decidim_awesome/admin/officializations/participants_th" %>
<% end %>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- insert_top '#advanced-settings-panel' -->

<% if Decidim::DecidimAwesome.enabled?(:admins_available_authorizations) %>
<div class="awesome_available_authorizations border-2 rounded border-background p-4 form__wrapper mt-8 first:mt-0 last:pb-4">
<h3 class="h4"><%= t "decidim.decidim_awesome.system.organizations.awesome_tweaks" %></h3>

<%= render partial: "decidim/decidim_awesome/system/organizations/admin_allowed_authorizations", locals: { f: f } %>
</div>
<% end %>
Loading

0 comments on commit 2d63fe5

Please sign in to comment.