Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[3260] Add feature to manage partner users for a Partner as a Organization/Bank #3266

Merged
merged 42 commits into from
Jun 19, 2024
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
fa7267f
WIP
edwinthinks Nov 21, 2022
2bf70e7
WIP
edwinthinks Nov 23, 2022
7543691
Merge branch 'main' into 3260/bank-partner-user-management
edwinthinks Nov 23, 2022
76d3559
Update with button
edwinthinks Nov 23, 2022
c70ed81
WIP
edwinthinks Nov 23, 2022
8a97f8c
WIP
edwinthinks Nov 23, 2022
59705a2
Update
edwinthinks Nov 23, 2022
3ba9e15
Updated
edwinthinks Nov 23, 2022
90d7440
Add and remove access to partner account for users
edwinthinks Nov 23, 2022
c7c4718
WIP
edwinthinks Nov 23, 2022
85b505c
WIP
edwinthinks Nov 24, 2022
f2799fc
WIP
edwinthinks Nov 24, 2022
2428e22
Re-styling
edwinthinks Nov 24, 2022
7d8bbad
WIP
edwinthinks Nov 25, 2022
530ef2d
Remove older method for inviting and reseting password for partner
edwinthinks Nov 25, 2022
1b7945d
Fix rubocop complaints
edwinthinks Nov 25, 2022
eb0c08e
Merge branch 'main' into 3260/bank-partner-user-management
edwinthinks Dec 18, 2022
98d7183
Merge branch 'main' into 3260/bank-partner-user-management
edwinthinks Jan 10, 2023
a77c123
Update styling on table
edwinthinks Jan 10, 2023
cc60914
Update with partner merge changes
edwinthinks Jan 10, 2023
4267d36
Use basic flow instead of turbo
Feb 8, 2023
49eeb4f
fix form errors
Feb 8, 2023
d43f14d
Merge branch 'main' into 3260/bank-partner-user-management
awwaiid Feb 24, 2023
f58b941
Merge remote-tracking branch 'origin/main' into 3260/bank-partner-use…
awwaiid Apr 2, 2023
846db60
Satiate Rubocop! [#3260]
awwaiid Apr 2, 2023
7804b87
Fix some typos causing reference error
edwinthinks Apr 8, 2023
8f15d3b
Prevent reference erro by using current_partner
edwinthinks Apr 8, 2023
1355926
Remove outdated test which uses old invite pathway
edwinthinks Apr 8, 2023
301aa3f
Update system test to use new partner/users route
edwinthinks Apr 8, 2023
f58d3e3
Fix rubocop
edwinthinks Apr 8, 2023
66b225a
Merge branch 'main' into 3260/bank-partner-user-management
edwinthinks Jun 25, 2023
1849a7c
Merge branch 'main' into 3260/bank-partner-user-management
edwinthinks Jul 16, 2023
20ea8d0
Bring back missing code
edwinthinks Jul 16, 2023
2900177
Remove extra locale that doesn't seem used
edwinthinks Jul 23, 2023
3067c47
Add missing request spec for new controller
edwinthinks Jul 23, 2023
136871d
Remove code to prevent broadcasting in model callbacks. Not sure why …
edwinthinks Jul 23, 2023
da15745
Merge branch 'main' into 3260/bank-partner-user-management
dorner May 24, 2024
b9bdf96
Lint + spec fix
dorner May 24, 2024
59cc42a
Fix
dorner May 24, 2024
e330716
Merge branch 'main' into 3260/bank-partner-user-management
dorner Jun 7, 2024
49d936c
CR fixes
dorner Jun 7, 2024
4128e4b
Fix spec
dorner Jun 14, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 63 additions & 0 deletions app/controllers/partner_users_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# frozen_String_literal: true

class PartnerUsersController < ApplicationController
before_action :set_partner, only: %i[index create destroy resend_invitation]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only link to this controller from bank admins, but we don't here restrict access to only admins. I'll open a separate ticket to address this minor security flaw.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Created #4458 to address


def index
@users = @partner.users
@user = User.new(name: "")
end

def create
@user = UserInviteService.invite(
email: user_params[:email],
name: user_params[:name],
roles: [Role::PARTNER],
resource: @partner
)
if @user.valid?
redirect_back(fallback_location: "/",
notice: "#{@user.name} has been invited. Invitation email sent to #{@user.email}")
else
flash[:alert] = "Invitation failed. Check the form for errors."
@users = @partner.users
render :index
end
end

def destroy
user = User.find(params[:id])

if user.remove_role(Role::PARTNER, @partner)
redirect_back(fallback_location: "/", notice: "Access to #{@partner.name} has been revoked for #{user.display_name}.")
else
redirect_back(fallback_location: "/", alert: "Invitation failed. Check the form for errors.")
end
end

def resend_invitation
user = User.find(params[:id])

if user.invitation_accepted_at.nil?
user.invite!
else
user.errors.add(:base, "User has already accepted invitation.")
end
Comment on lines +41 to +45
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can replace this with UserInviteService just like above, just pass force: true.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep I think so!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'am actually thinking that a new method makes sense, maybe self.resend_invite? It feels like that method is getting more complex for something that we just want to resend the invite (not worry about Roles). What do you think?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fair - we can add a separate method to the service that only works on an existing user and doesn't change the roles.


if user.errors.none?
redirect_back(fallback_location: "/", notice: "Invitation email sent to #{user.email}")
else
redirect_back(fallback_location: "/", alert: user.errors.full_messages.to_sentence)
end
end

private

def set_partner
@partner = Partner.find(params[:partner_id])
end

def user_params
params.require(:user).permit(:email, :name)
end
end
12 changes: 0 additions & 12 deletions app/controllers/partners_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -129,18 +129,6 @@ def invite
end
end

def invite_partner_user
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still referenced from config/routes.rb, but nothing submits to it, so not an immediate blocker

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I created #4459 to address

partner = current_organization.partners.find(params[:partner])
UserInviteService.invite(name: params[:name],
email: params[:email],
roles: [Role::PARTNER],
resource: partner)

redirect_to partner_path(partner), notice: "We have invited #{params[:email]} to #{partner.name}!"
rescue StandardError => e
redirect_to partner_path(partner), error: "Failed to invite #{params[:email]} to #{partner.name} due to: #{e.message}"
end

def recertify_partner
@partner = current_organization.partners.find(params[:id])

Expand Down
20 changes: 20 additions & 0 deletions app/views/partner_users/_form.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<div class="card card-primary">
<div class="card-header">
<h3 class="card-title">Invite New User</h3>
</div>

<%= simple_form_for user, url: partner_users_path(partner) do |f| %>
<div class="card-body">
<div class="form-group">
<%= f.input :name, label: "Name", placeholder: "Name", required: true %>
</div>
<div class="form-group">
<%= f.input :email, label: "Email", placeholder: "Email", required: true %>
</div>
</div>

<div class="card-footer">
<%= f.submit "Invite User", class: 'btn btn-primary float-right' %>
</div>
<% end %>
</div>
32 changes: 32 additions & 0 deletions app/views/partner_users/_user.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<tr>
<td><%= user.id %></td>
<td><%= user.name %></td>
<td><%= user.email %></td>
<td>
<% if user.last_sign_in_at.present? %>
<%= user.last_sign_in_at.strftime('%B %-d, %Y') %>
<% else %>
<span class="badge bg-danger">Never</span>
<% end %>
</td>
<td>
<% if user.invitation_accepted_at %>
<span class="badge bg-success">Accepted</span>
<% else %>
<div class='d-flex flex-column justify-content-center align-items-start'>
<span class="badge bg-warning">
<div>Waiting Acceptance</div>
</span>
<small>Last invite sent
<strong><%= time_ago_in_words(user.invitation_sent_at) %> ago</strong> on <%= user.invitation_sent_at.strftime('%B %-d, %Y') %>
</small>
</div>
<% end %>
</td>
<td class='d-flex'>
<% unless user.invitation_accepted_at %>
<%= button_to "Resend Invite", resend_invitation_partner_user_path(partner, user), method: :post, class: "btn btn-primary btn-sm mr-2" %>
<% end %>
<%= button_to "Remove Access", partner_user_path(partner, user), method: :delete, data: { confirm: "Are you sure you want to remove access to #{user.name} to access #{partner.name}?" }, class: "btn btn-danger btn-sm" %>
</td>
</tr>
66 changes: 66 additions & 0 deletions app/views/partner_users/_users.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<div class="card card-success">
<div class="card-header">
<h3 class="card-title">Users</h3>
</div>

<div class="card-body table-responsive p-0">
<table class="table table-hover text-nowrap">
<thead>
<tr>
<th>Name & Email</th>
<th>Last Login</th>
<th>Invitation Status</th>
<th>Action</th>
</tr>
</thead>
<tbody>
<% users.order(name: :asc).each do |user| %>
<tr>
<td>
<div class='d-flex flex-column'>
<span><%= user.name %></span>
<small><%= user.email %></small>
</div>
</td>
<td>
<small>
<% if user.last_sign_in_at.present? %>
<%= time_ago_in_words(user.last_sign_in_at) %> ago <small>(<%= user.invitation_sent_at.strftime('%b %d %Y') %>)</small>
<% else %>
<span class="badge bg-danger">Never</span>
<% end %>
</small>
</td>
<td>
<% if user.invitation_accepted_at %>
<span class="badge bg-success">
<i class="fas fa-check"></i> Accepted
</span>
<% else %>
<div class='d-flex flex-column justify-content-center align-items-start'>
<span class="badge bg-warning">
<div>Waiting Acceptance</div>
</span>
<small style='font-size:10px'>
Last invite sent <strong><%= time_ago_in_words(user.invitation_sent_at) %> ago on <%= user.invitation_sent_at.strftime('%b %d %Y') %></strong>
</small>
</div>
<% end %>
</td>
<td class='d-flex flex-column'>
<% unless user.invitation_accepted_at %>
<%= button_to resend_invitation_partner_user_path(partner, user), method: :post, class: "btn btn-warning btn-xs mb-2" do %>
<i class="fa fa-envelope"></i> Resend Invitation
<% end %>
<% end %>
<%= button_to partner_user_path(partner, user), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-danger btn-xs" do %>
<i class="fa fa-ban"></i> Remove Access
<% end %>

</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
40 changes: 40 additions & 0 deletions app/views/partner_users/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<section class="content-header">
<div class="container-fluid">
<div class="row mb-2">
<div class="col-sm-6">
<% content_for :title, "Partner | Users" %>
<h1>
<%= @partner.name %>'s Users
</h1>
</div>
<div class="col-sm-6">
<ol class="breadcrumb float-sm-right">
<li class="breadcrumb-item"><%= link_to(dashboard_path) do %>
<i class="fa fa-dashboard"></i> Home
<% end %>
</li>
<li class="breadcrumb-item">
<%= link_to(@partner) do %>
<%= @partner.name %>
<% end %>
</li>
<li class="breadcrumb-item">Users</li>
</ol>
</div>
</div>
</div><!-- /.container-fluid -->
</section>

<section class="content">
<div class="container-fluid">
<div class="row">
<div class="col-md-9">
<%= render 'users', users: @users, partner: @partner %>
</div>

<div class="col-md-3">
<%= render 'form', user: @user, partner: @partner %>
</div>
</div>
</div>
</section>
107 changes: 8 additions & 99 deletions app/views/partners/show.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@
<h2 class="card-title">Partner Actions</h2>
</div>
<div class="card-body p-3">
<% if current_user.has_role?(Role::ORG_ADMIN, current_organization) %>
<%= link_to partner_users_path(@partner) do %>
<div class="btn btn-app bg-success">
<i class="fas fa-users"></i> Manage Users
</div>
<% end %>
<% end %>
<hr>
<div class="row px-2">
<div class="col-lg-4 col-sm-12">
<div>
Expand All @@ -44,15 +52,6 @@
<br>
<%= view_button_to '#partner-information', { text: "View Partner Information", type: "secondary", size: "m" } %>
</div>
<div class="col-lg-4 col-sm-12">
<div>
<h4 class='text-2xl underline'> Partner Membership </h4>
<%= modal_button_to("#resetPasswordModal", { text: "Reset Partner Password", icon: "check", type: "warning", size: "m" }) if can_administrate? %>
<br>
<br>
<%= modal_button_to("#addUserModal", { text: "Add User To Partner", icon: "plus", type: "warning", size: "m" }) if can_administrate? %>
</div>
</div>
<div class="col-lg-4 col-sm-12">
<div>
<h4 class='text-2xl underline'> Partner Status </h4>
Expand Down Expand Up @@ -135,37 +134,6 @@
</div>
</div>
</section>

<section class="card card-info card-outline" id="partner-users">
<div class="card-header">
<h2 class="card-title">Users who can access this Partner</h2>
</div>
<div class="card-body p-0">
<div class="tab-content" id="custom-tabs-three-tabContent">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Invitation Sent</th>
<th>Last Logged In</th>
</tr>
</thead>
<tbody>
<% @partner_users.each do |user| %>
<tr>
<td><%= user.display_name %></td>
<td><%= user.email %></td>
<td><%= user.invitation_sent_at && user.invitation_sent_at.strftime('%B %-d, %Y') %></td>
<td><%= user.last_sign_in_at && user.last_sign_in_at.strftime('%B %-d, %Y') %></td>
</tr>
</tbody>
<% end %>
</table>
</div><!-- /.box-body.table-responsive -->
</div>
</section>

<section class="card card-info card-outline">
<div class="card-header">
<h2 class="card-title">Settings</h2>
Expand Down Expand Up @@ -288,63 +256,4 @@
</div>
</div>

<div id="addUserModal" class="modal fade">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Add a User to <%= @partner.name %></h4>
</div><!-- modal-header -->
<div class="modal-body">
<div class="box-body">
<p>
This will invite an additional member of the partner organization to become a partner user in the system. This user will show up on the lists of users who can access this partner after being invited and you will be able to track their most recent log in as well.
</p>
<br>
<%= form_tag invite_partner_user_partner_path do %>
<div class="input-group">
<span class="input-group-text" id="spn_env_fa_icon"><%= fa_icon "envelope" %></span>
<input type="email" name="email" class="form-control" placeholder="Email" aria-describedby="spn_env_fa_icon" required autocomplete="off">
</div>
<div class="input-group mt-3">
<span class="input-group-text" id="spn_env_fa_icon"><%= fa_icon "user" %></span>
<input type="text" name="name" class="form-control" placeholder="Name" aria-describedby="name_icon" autocomplete="off">
<%= hidden_field_tag :partner, @partner.id %><br>
</div>
<br>
<%= submit_button({ text: "Invite User", icon: "envelope" }) %>
<% end # form %>
</div><!-- box-body -->
</div><!-- modal-body -->
</div><!-- modal-content -->
</div><!-- modal-dialog -->
</div><!-- addUserModal -->
<div id="resetPasswordModal" class="modal fade">
<div class="modal-dialog">
<!-- Modal content-->
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Reset Password <%= @partner.name %></h4>
</div><!-- modal-header -->
<div class="modal-body">
<div class="box-body">
<p>
If your partner is unable to log in because they have forgotten their password, this will send your partner an email to reset their password so are able to can log in again. The person receiving this email should exist in the list of users who can access this partner. That list is available on the current page <a href="#partner-users">here.</a>
</p>
<br>
<%= form_tag partner_user_reset_password_users_path do %>
<div class="input-group">
<span class="input-group-text" id="spn_env_fa_icon"><%= fa_icon "envelope" %></span>
<input type="email" name="email" class="form-control" placeholder="Email" aria-describedby="spn_env_fa_icon" autocomplete="off">
<%= hidden_field_tag :partner_id, @partner.id %><br>
<%= hidden_field_tag :organization_name, current_organization %>
</div>
<br>
<%= submit_button({ text: "Reset Password", icon: "envelope" }) %>
<% end # form %>
</div><!-- box-body -->
</div><!-- modal-body -->
</div><!-- modal-content -->
</div><!-- modal-dialog -->
</div><!-- addUserModal -->
</section>
3 changes: 1 addition & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,4 @@
# To learn more, please read the Rails Internationalization guide
# available at http://guides.rubyonrails.org/i18n.html.

en:
hello: "Hello world"
en:
2 changes: 2 additions & 0 deletions config/locales/roles.en.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
roles:
partner: 'Regular'
Loading
Loading