feat: Add APIs to manage custom roles in Chatwoot (#9995)

Co-authored-by: Pranav <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose
2024-08-23 04:48:28 -07:00
committed by GitHub
parent 41c5e7d3f1
commit b61ad6e41a
24 changed files with 440 additions and 18 deletions

View File

@@ -24,7 +24,7 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
def update def update
@agent.update!(agent_params.slice(:name).compact) @agent.update!(agent_params.slice(:name).compact)
@agent.current_account_user.update!(agent_params.slice(:role, :availability, :auto_offline).compact) @agent.current_account_user.update!(agent_params.slice(*account_user_attributes).compact)
end end
def destroy def destroy
@@ -67,8 +67,16 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
@agent = agents.find(params[:id]) @agent = agents.find(params[:id])
end end
def account_user_attributes
[:role, :availability, :auto_offline]
end
def allowed_agent_params
[:name, :email, :name, :role, :availability, :auto_offline]
end
def agent_params def agent_params
params.require(:agent).permit(:name, :email, :name, :role, :availability, :auto_offline) params.require(:agent).permit(allowed_agent_params)
end end
def new_agent_params def new_agent_params
@@ -101,3 +109,5 @@ class Api::V1::Accounts::AgentsController < Api::V1::Accounts::BaseController
DeleteObjectJob.perform_later(agent) if agent.reload.account_users.blank? DeleteObjectJob.perform_later(agent) if agent.reload.account_users.blank?
end end
end end
Api::V1::Accounts::AgentsController.prepend_mod_with('Api::V1::Accounts::AgentsController')

View File

@@ -2,22 +2,24 @@
# #
# Table name: account_users # Table name: account_users
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# active_at :datetime # active_at :datetime
# auto_offline :boolean default(TRUE), not null # auto_offline :boolean default(TRUE), not null
# availability :integer default("online"), not null # availability :integer default("online"), not null
# role :integer default("agent") # role :integer default("agent")
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :bigint # account_id :bigint
# inviter_id :bigint # custom_role_id :bigint
# user_id :bigint # inviter_id :bigint
# user_id :bigint
# #
# Indexes # Indexes
# #
# index_account_users_on_account_id (account_id) # index_account_users_on_account_id (account_id)
# index_account_users_on_user_id (user_id) # index_account_users_on_custom_role_id (custom_role_id)
# uniq_user_id_per_account_id (account_id,user_id) UNIQUE # index_account_users_on_user_id (user_id)
# uniq_user_id_per_account_id (account_id,user_id) UNIQUE
# #
class AccountUser < ApplicationRecord class AccountUser < ApplicationRecord
@@ -77,4 +79,6 @@ class AccountUser < ApplicationRecord
end end
end end
AccountUser.prepend_mod_with('AccountUser')
AccountUser.include_mod_with('Audit::AccountUser') AccountUser.include_mod_with('Audit::AccountUser')
AccountUser.include_mod_with('Concerns::AccountUser')

View File

@@ -10,3 +10,4 @@ json.custom_attributes resource.custom_attributes if resource.custom_attributes.
json.name resource.name json.name resource.name
json.role resource.role json.role resource.role
json.thumbnail resource.avatar_url json.thumbnail resource.avatar_url
json.custom_role_id resource.current_account_user&.custom_role_id if ChatwootApp.enterprise?

View File

@@ -74,6 +74,7 @@ Rails.application.routes.draw do
post :execute, on: :member post :execute, on: :member
end end
resources :sla_policies, only: [:index, :create, :show, :update, :destroy] resources :sla_policies, only: [:index, :create, :show, :update, :destroy]
resources :custom_roles, only: [:index, :create, :show, :update, :destroy]
resources :campaigns, only: [:index, :create, :show, :update, :destroy] resources :campaigns, only: [:index, :create, :show, :update, :destroy]
resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy] resources :dashboard_apps, only: [:index, :show, :create, :update, :destroy]
namespace :channels do namespace :channels do

View File

@@ -0,0 +1,16 @@
class AddCustomRoles < ActiveRecord::Migration[7.0]
def change
# Create the roles table
create_table :custom_roles do |t|
t.string :name
t.string :description
t.references :account, null: false
t.text :permissions, array: true, default: []
t.timestamps
end
# Associate the custom role with account user
# Add the custom_role_id column to the account_users table
add_reference :account_users, :custom_role, optional: true
end
end

View File

@@ -10,7 +10,7 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2024_05_16_003531) do ActiveRecord::Schema[7.0].define(version: 2024_07_26_220747) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements" enable_extension "pg_stat_statements"
enable_extension "pg_trgm" enable_extension "pg_trgm"
@@ -37,8 +37,10 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_16_003531) do
t.datetime "active_at", precision: nil t.datetime "active_at", precision: nil
t.integer "availability", default: 0, null: false t.integer "availability", default: 0, null: false
t.boolean "auto_offline", default: true, null: false t.boolean "auto_offline", default: true, null: false
t.bigint "custom_role_id"
t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true
t.index ["account_id"], name: "index_account_users_on_account_id" t.index ["account_id"], name: "index_account_users_on_account_id"
t.index ["custom_role_id"], name: "index_account_users_on_custom_role_id"
t.index ["user_id"], name: "index_account_users_on_user_id" t.index ["user_id"], name: "index_account_users_on_user_id"
end end
@@ -538,6 +540,16 @@ ActiveRecord::Schema[7.0].define(version: 2024_05_16_003531) do
t.index ["user_id"], name: "index_custom_filters_on_user_id" t.index ["user_id"], name: "index_custom_filters_on_user_id"
end end
create_table "custom_roles", force: :cascade do |t|
t.string "name"
t.string "description"
t.bigint "account_id", null: false
t.text "permissions", default: [], array: true
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["account_id"], name: "index_custom_roles_on_account_id"
end
create_table "dashboard_apps", force: :cascade do |t| create_table "dashboard_apps", force: :cascade do |t|
t.string "title", null: false t.string "title", null: false
t.jsonb "content", default: [] t.jsonb "content", default: []

View File

@@ -0,0 +1,31 @@
class Api::V1::Accounts::CustomRolesController < Api::V1::Accounts::EnterpriseAccountsController
before_action :fetch_custom_role, only: [:show, :update, :destroy]
before_action :check_authorization
def index
@custom_roles = Current.account.custom_roles
end
def create
@custom_role = Current.account.custom_roles.create!(permitted_params)
end
def show; end
def update
@custom_role.update!(permitted_params)
end
def destroy
@custom_role.destroy!
head :ok
end
def permitted_params
params.require(:custom_role).permit(:name, :description, permissions: [])
end
def fetch_custom_role
@custom_role = Current.account.custom_roles.find_by(id: params[:id])
end
end

View File

@@ -0,0 +1,9 @@
module Enterprise::Api::V1::Accounts::AgentsController
def account_user_attributes
super + [:custom_role_id]
end
def allowed_agent_params
super + [:custom_role_id]
end
end

View File

@@ -0,0 +1,42 @@
# == Schema Information
#
# Table name: custom_roles
#
# id :bigint not null, primary key
# description :string
# name :string
# permissions :text default([]), is an Array
# created_at :datetime not null
# updated_at :datetime not null
# account_id :bigint not null
#
# Indexes
#
# index_custom_roles_on_account_id (account_id)
#
#
# Available permissions for custom roles:
# - 'conversation_manage': Can manage all conversations.
# - 'conversation_unassigned_manage': Can manage unassigned conversations and assign to self.
# - 'conversation_participating_manage': Can manage conversations they are participating in (assigned to or a participant).
# - 'contact_manage': Can manage contacts.
# - 'report_manage': Can manage reports.
# - 'knowledge_base_manage': Can manage knowledge base portals.
class CustomRole < ApplicationRecord
belongs_to :account
has_many :account_users, dependent: :nullify
PERMISSIONS = %w[
conversation_manage
conversation_unassigned_manage
conversation_participating_manage
contact_manage
report_manage
knowledge_base_manage
].freeze
validates :name, presence: true
validates :permissions, inclusion: { in: PERMISSIONS }
end

View File

@@ -0,0 +1,5 @@
module Enterprise::AccountUser
def permissions
custom_role.present? ? (custom_role.permissions + ['custom_role']) : super
end
end

View File

@@ -4,6 +4,7 @@ module Enterprise::Concerns::Account
included do included do
has_many :sla_policies, dependent: :destroy_async has_many :sla_policies, dependent: :destroy_async
has_many :applied_slas, dependent: :destroy_async has_many :applied_slas, dependent: :destroy_async
has_many :custom_roles, dependent: :destroy_async
def self.add_response_related_associations def self.add_response_related_associations
has_many :response_sources, dependent: :destroy_async has_many :response_sources, dependent: :destroy_async

View File

@@ -0,0 +1,7 @@
module Enterprise::Concerns::AccountUser
extend ActiveSupport::Concern
included do
belongs_to :custom_role, optional: true
end
end

View File

@@ -0,0 +1,21 @@
class CustomRolePolicy < ApplicationPolicy
def index?
@account_user.administrator?
end
def update?
@account_user.administrator?
end
def show?
@account_user.administrator?
end
def create?
@account_user.administrator?
end
def destroy?
@account_user.administrator?
end
end

View File

@@ -0,0 +1 @@
json.partial! 'api/v1/models/custom_role', formats: [:json], custom_role: @custom_role

View File

@@ -0,0 +1,3 @@
json.array! @custom_roles do |custom_role|
json.partial! 'api/v1/models/custom_role', formats: [:json], custom_role: custom_role
end

View File

@@ -0,0 +1 @@
json.partial! 'api/v1/models/custom_role', formats: [:json], custom_role: @custom_role

View File

@@ -0,0 +1 @@
json.partial! 'api/v1/models/custom_role', formats: [:json], custom_role: @custom_role

View File

@@ -0,0 +1,6 @@
json.id custom_role.id
json.name custom_role.name
json.description custom_role.description
json.permissions custom_role.permissions
json.created_at custom_role.created_at
json.updated_at custom_role.updated_at

View File

@@ -0,0 +1,174 @@
require 'rails_helper'
RSpec.describe 'Custom Roles API', type: :request do
let!(:account) { create(:account) }
let!(:administrator) { create(:user, account: account, role: :administrator) }
let!(:agent) { create(:user, account: account, role: :agent) }
let!(:custom_role) { create(:custom_role, account: account, name: 'Manager') }
describe 'GET #index' do
context 'when it is an authenticated administrator' do
it 'returns all custom roles in the account' do
get "/api/v1/accounts/#{account.id}/custom_roles",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body[0]).to include('name' => custom_role.name)
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'GET #show' do
context 'when it is an authenticated administrator' do
it 'returns the custom role details' do
get "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('name' => custom_role.name)
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
get "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'POST #create' do
let(:valid_params) do
{ custom_role: { name: 'Support', description: 'Support role', permissions: CustomRole::PERMISSIONS.sample(SecureRandom.random_number(4)) } }
end
context 'when it is an authenticated administrator' do
it 'creates the custom role' do
expect do
post "/api/v1/accounts/#{account.id}/custom_roles",
params: valid_params,
headers: administrator.create_new_auth_token
end.to change(CustomRole, :count).by(1)
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('name' => 'Support')
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/custom_roles",
params: valid_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/custom_roles",
params: valid_params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'PUT #update' do
let(:update_params) { { custom_role: { name: 'Updated Role' } } }
context 'when it is an authenticated administrator' do
it 'updates the custom role' do
put "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
params: update_params,
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
body = JSON.parse(response.body)
expect(body).to include('name' => 'Updated Role')
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
params: update_params,
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
put "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
params: update_params
expect(response).to have_http_status(:unauthorized)
end
end
end
describe 'DELETE #destroy' do
context 'when it is an authenticated administrator' do
it 'deletes the custom role' do
delete "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: administrator.create_new_auth_token
expect(response).to have_http_status(:success)
expect(CustomRole.count).to eq(0)
end
end
context 'when the user is an agent and is authenticated' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}",
headers: agent.create_new_auth_token
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/custom_roles/#{custom_role.id}"
expect(response).to have_http_status(:unauthorized)
end
end
end
end

View File

@@ -0,0 +1,24 @@
require 'rails_helper'
RSpec.describe 'Enterprise Agents API', type: :request do
let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) }
describe 'PUT /api/v1/accounts/{account.id}/agents/:id' do
let(:other_agent) { create(:user, account: account, role: :agent) }
let!(:custom_role) { create(:custom_role, account: account) }
context 'when it is an authenticated administrator' do
it 'modified the custom role of the agent' do
put "/api/v1/accounts/#{account.id}/agents/#{other_agent.id}",
headers: admin.create_new_auth_token,
params: { custom_role_id: custom_role.id },
as: :json
expect(response).to have_http_status(:success)
expect(other_agent.account_users.first.reload.custom_role_id).to eq(custom_role.id)
expect(JSON.parse(response.body)['custom_role_id']).to eq(custom_role.id)
end
end
end
end

View File

@@ -2,9 +2,15 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe Account do RSpec.describe Account, type: :model do
include ActiveJob::TestHelper include ActiveJob::TestHelper
describe 'associations' do
it { is_expected.to have_many(:sla_policies).dependent(:destroy_async) }
it { is_expected.to have_many(:applied_slas).dependent(:destroy_async) }
it { is_expected.to have_many(:custom_roles).dependent(:destroy_async) }
end
describe 'sla_policies' do describe 'sla_policies' do
let!(:account) { create(:account) } let!(:account) { create(:account) }
let!(:sla_policy) { create(:sla_policy, account: account) } let!(:sla_policy) { create(:sla_policy, account: account) }

View File

@@ -2,7 +2,33 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe AccountUser do RSpec.describe AccountUser, type: :model do
describe 'associations' do
# option and dependant nullify
it { is_expected.to belong_to(:custom_role).optional }
end
describe 'permissions' do
context 'when custom role is assigned' do
it 'returns permissions of the custom role along with `custom_role` permission' do
account = create(:account)
custom_role = create(:custom_role, account: account)
account_user = create(:account_user, account: account, custom_role: custom_role)
expect(account_user.permissions).to eq(custom_role.permissions + ['custom_role'])
end
end
context 'when custom role is not assigned' do
it 'returns permissions of the default role' do
account = create(:account)
account_user = create(:account_user, account: account)
expect(account_user.permissions).to eq([account_user.role])
end
end
end
describe 'audit log' do describe 'audit log' do
context 'when account user is created' do context 'when account user is created' do
it 'has associated audit log created' do it 'has associated audit log created' do

View File

@@ -0,0 +1,12 @@
require 'rails_helper'
RSpec.describe CustomRole, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:account) }
it { is_expected.to have_many(:account_users).dependent(:nullify) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:name) }
end
end

View File

@@ -0,0 +1,8 @@
FactoryBot.define do
factory :custom_role do
account
name { Faker::Name.name }
description { Faker::Lorem.sentence }
permissions { CustomRole::PERMISSIONS.sample(SecureRandom.random_number(4)) }
end
end