Skip to content

Commit

Permalink
Merge pull request #3720 from rubyforgood/3554-superadmin-roles
Browse files Browse the repository at this point in the history
3554 Allow superadmins to manage roles
  • Loading branch information
awwaiid authored Jul 16, 2023
2 parents 6dee551 + 257b4ff commit 8c4c6f4
Show file tree
Hide file tree
Showing 16 changed files with 473 additions and 39 deletions.
40 changes: 40 additions & 0 deletions app/controllers/admin/users_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def new

def edit
@user = User.find_by(id: params[:id])
@resources = Role.resources_for_select
end

def create
Expand All @@ -56,6 +57,45 @@ def destroy
end
end

def resource_ids
klass = case params[:resource_type]
when "org_admin", "org_user"
Organization
when "partner"
Partner
else
raise "Unknown resource type #{params[:resource_type]}"
end

objects = klass.where("name LIKE ?", "%#{params[:q]}%").select(:id, :name)
object_json = objects.map do |obj|
{
id: obj.id,
text: obj.name
}
end
render json: { results: object_json }
end

def add_role
begin
AddRoleService.call(user_id: params[:user_id],
resource_type: params[:resource_type],
resource_id: params[:resource_id])
rescue => e
redirect_back(fallback_location: admin_users_path, alert: e.message)
return
end
redirect_back(fallback_location: admin_users_path, notice: "Role added!")
end

def remove_role
RemoveRoleService.call(user_id: params[:user_id], role_id: params[:role_id])
redirect_back(fallback_location: admin_users_path, notice: "Role removed!")
rescue => e
redirect_back(fallback_location: admin_users_path, alert: e.message)
end

private

def user_params
Expand Down
6 changes: 4 additions & 2 deletions app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def current_organization
return @current_organization if @current_organization
return nil unless current_role

return current_role.resource if current_role.resource.is_a?(Organization)
return current_role.resource if current_role&.resource&.is_a?(Organization)

Organization.find_by(short_name: params[:organization_id])
end
Expand Down Expand Up @@ -52,7 +52,7 @@ def organization_url_options(options = {})
def default_url_options(options = {})
# Early return if the request is not authenticated and no
# current_user is defined
return options if current_user.blank?
return options if current_user.blank? || current_role.blank?

if current_organization.present? && !options.key?(:organization_id)
options[:organization_id] = current_organization.to_param
Expand All @@ -66,6 +66,8 @@ def default_url_options(options = {})
end

def dashboard_path_from_current_role
return root_path if current_role.blank?

if current_role.name == Role::SUPER_ADMIN.to_s
admin_dashboard_path
elsif current_role.name == Role::PARTNER.to_s
Expand Down
26 changes: 16 additions & 10 deletions app/controllers/organizations_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,27 @@ def resend_user_invitation
def promote_to_org_admin
user = User.find(params[:user_id])
raise ActiveRecord::RecordNotFound unless user.has_role?(Role::ORG_USER, current_organization)
user.add_role(Role::ORG_ADMIN, current_organization)
redirect_to user_update_redirect_path, notice: "User has been promoted!"
begin
AddRoleService.call(user_id: user.id,
resource_type: Role::ORG_ADMIN,
resource_id: current_organization.id)
redirect_to user_update_redirect_path, notice: "User has been promoted!"
rescue => e
redirect_back(fallback_location: organization_path(current_organization), alert: e.message)
end
end

def demote_to_user
user = User.find(params[:user_id])
raise ActiveRecord::RecordNotFound unless user.has_role?(Role::ORG_USER, current_organization)
if user.has_role?(Role::SUPER_ADMIN)
notice = "Unable to convert super to user."
else
user.remove_role(Role::ORG_ADMIN, current_organization)
notice = "Admin has been changed to User!"
raise ActiveRecord::RecordNotFound unless user.has_role?(Role::ORG_ADMIN, current_organization)
begin
RemoveRoleService.call(user_id: params[:user_id],
resource_type: Role::ORG_ADMIN,
resource_id: current_organization.id)
redirect_to user_update_redirect_path, notice: notice
rescue => e
redirect_back(fallback_location: organization_path(current_organization), alert: e.message)
end

redirect_to user_update_redirect_path, notice: notice
end

def deactivate_user
Expand Down
34 changes: 34 additions & 0 deletions app/javascript/controllers/double_select_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { Controller } from "@hotwired/stimulus"
import $ from 'jquery';
import "select2"

export default class extends Controller {
static targets = ['source', 'destination']
static values = {
url: String
}

sourceChanged() {
const val = $(this.sourceTarget).val()
const url = new URL(this.urlValue)
url.searchParams.append('resource_type', val);
$(this.destinationTarget).select2({
ajax: {
url: url.toString(),
dataType: 'json'
}
});

}

connect() {
/**
* This is a workaround to auto focus on the select2 input when it is opened.
*/
$(this.destinationTarget).on('select2:open', function (e) {
$(".select2-search__field")[0].focus();
})
this.sourceChanged();
}

}
23 changes: 23 additions & 0 deletions app/models/role.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,27 @@ class Role < ApplicationRecord
ORG_ADMIN = :org_admin
SUPER_ADMIN = :super_admin
PARTNER = :partner

TITLES = {
org_user: "Organization",
org_admin: "Organization Admin",
partner: "Partner",
super_admin: "Super admin"
}.freeze

TITLE_TO_RESOURCE = {
org_user: ::Organization,
org_admin: ::Organization,
partner: ::Partner
}.freeze

# @return [String]
def title
TITLES[name.to_sym]
end

# @return [Hash<Symbol, String>]
def self.resources_for_select
TITLES.without(:super_admin).invert
end
end
29 changes: 29 additions & 0 deletions app/services/add_role_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class AddRoleService
# @param user_id [Integer]
# @param resource_id [Integer]
# @param resource_type [String]
def self.call(user_id:, resource_type:, resource_id: nil)
user = User.find(user_id)
if resource_type.to_sym == Role::SUPER_ADMIN
add_super_admin(user)
return
end
klass = Role::TITLE_TO_RESOURCE[resource_type.to_sym]
resource = klass.find(resource_id)
if user.has_role?(resource_type, resource)
raise "User #{user.name} already has role for #{resource.name}"
end
user.add_role(resource_type, resource)
if resource_type.to_sym == Role::ORG_ADMIN
user.add_role(:org_user, resource)
end
end

# @param user [User]
def self.add_super_admin(user)
if user.has_role?(Role::SUPER_ADMIN)
raise "User #{user.name} already has super admin role!"
end
user.add_role(Role::SUPER_ADMIN)
end
end
28 changes: 28 additions & 0 deletions app/services/remove_role_service.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
class RemoveRoleService
# @param user_id [Integer]
# @param role_id [Integer]
# @param resource_type [String]
# @param resource_id [Integer]
def self.call(user_id:, role_id: nil, resource_type: nil, resource_id: nil)
if role_id.nil? && resource_id.nil?
raise "Must provide either a role ID or resource ID!"
end
if role_id.nil?
role_id = Role.find_by(name: resource_type, resource_id: resource_id).id
end
user_role = UsersRole.find_by(user_id: user_id, role_id: role_id)
unless user_role
user = User.find(user_id)
role = Role.find(role_id)
raise "User #{user.name} does not have role for #{role.resource.name}!"
end

user_role.destroy
if user_role.role.name.to_sym == Role::ORG_USER # they can't be an admin if they're not a user
admin_role = Role.find_by(resource_id: user_role.role.resource_id, name: Role::ORG_ADMIN)
if admin_role
UsersRole.find_by(user_id: user_id, role_id: admin_role.id)&.destroy
end
end
end
end
68 changes: 68 additions & 0 deletions app/views/admin/users/_roles.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<section class="content">
<div class="container-fluid">
<div class="row">
<!-- left column -->
<div class="col-md-12">
<!-- jquery validation -->
<div class="card card-primary card-outline">
<div class="card-header">
<h5 class="card-title"><%= user.name %> - Roles
</h5>
</div>
<!-- /.card-header -->
<!-- form start -->
<div class="card-body">
<table class="table">
<thead>
<tr>
<th>Role Type</th>
<th>Resource</th>
<th class="text-right">Actions</th>
</tr>
</thead>
<tbody>
<% user.roles.each do |role| %>
<tr>
<td><%= role.title %></td>
<td><%= link_to role.resource.name, role.resource %></td>
<td class="text-right">
<%= delete_button_to admin_user_remove_role_path(user, role_id: role.id),
confirm: "Are you sure you want to remove this role?" %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div data-controller="double-select" data-double-select-url-value="<%= resource_ids_admin_users_url %>">
<h3>Add Role</h3>
<%= form_tag admin_user_add_role_path(user) do %>
<div class="form-inputs">
<div class="form-group">
<label>Type</label>
<div class="input-group">
<%= select_tag :resource_type, options_for_select(@resources),
class: 'select form-control',
data: { 'double-select-target': 'source',
'action': 'double-select#sourceChanged'
}
%>
</div>
</div>
<div class="form-group">
<label>Resource</label>
<div class="input-group">
<%= select_tag :resource_id, [], class: 'form-control', data: {
'double-select-target': 'destination'
} %>
</div>
</div>
</div>
<%= submit_tag 'Add Role', class: 'btn btn-md btn-primary' %>
<% end %>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
5 changes: 5 additions & 0 deletions app/views/admin/users/edit.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,8 @@
<!-- /.row -->
</div><!-- /.container-fluid -->
</section>

<% unless @user == current_user %>
<%= render partial: 'roles', object: @user, as: :user %>
<% end %>

6 changes: 5 additions & 1 deletion config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ def set_up_flipper
resources :base_items
resources :organizations
resources :partners, except: %i[new create]
resources :users
resources :users do
delete :remove_role
post :add_role
get :resource_ids, on: :collection
end
resources :barcode_items
resources :account_requests, only: [:index] do
post :reject, on: :collection
Expand Down
10 changes: 8 additions & 2 deletions spec/factories/users.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,19 @@
end

after(:create) do |user, evaluator|
user.add_role(Role::ORG_USER, evaluator.organization)
if evaluator.organization
user.add_role(Role::ORG_USER, evaluator.organization)
end
end

factory :organization_admin do
name { "Very Organized Admin" }
after(:create) do |user, evaluator|
user.add_role(Role::ORG_ADMIN, evaluator.organization)
if evaluator.organization
AddRoleService.call(user_id: user.id,
resource_id: evaluator.organization.id,
resource_type: Role::ORG_ADMIN)
end
end
end

Expand Down
Loading

0 comments on commit 8c4c6f4

Please sign in to comment.