feat: Platform API improvements (#2900)

- Platform APIs to add and update custom attributes to users
- Platform APIs to delete accounts
- Platform APIs to delete users
This commit is contained in:
Sojan Jose 2021-09-02 18:29:45 +05:30 committed by GitHub
parent a60a33679f
commit ad83d1bb71
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 160 additions and 12 deletions

View file

@ -16,7 +16,7 @@ class Platform::Api::V1::AccountsController < PlatformController
end end
def destroy def destroy
# TODO: obfusicate account DeleteObjectJob.perform_later(@resource)
head :ok head :ok
end end

View file

@ -19,21 +19,33 @@ class Platform::Api::V1::UsersController < PlatformController
def show; end def show; end
def update def update
@resource.update!(user_params) @resource.assign_attributes(user_update_params)
@resource.save!
end end
def destroy def destroy
# TODO: obfusicate user DeleteObjectJob.perform_later(@resource)
head :ok head :ok
end end
private private
def user_custom_attributes
return @resource.custom_attributes.merge(user_params[:custom_attributes]) if user_params[:custom_attributes]
@resource.custom_attributes
end
def user_update_params
# we want the merged custom attributes not the original one
user_params.except(:custom_attributes).merge({ custom_attributes: user_custom_attributes })
end
def set_resource def set_resource
@resource = User.find(params[:id]) @resource = User.find(params[:id])
end end
def user_params def user_params
params.permit(:name, :email, :password) params.permit(:name, :email, :password, custom_attributes: {})
end end
end end

View file

@ -0,0 +1,7 @@
class DeleteObjectJob < ApplicationJob
queue_as :default
def perform(object)
object.destroy!
end
end

View file

@ -9,6 +9,7 @@
# confirmed_at :datetime # confirmed_at :datetime
# current_sign_in_at :datetime # current_sign_in_at :datetime
# current_sign_in_ip :string # current_sign_in_ip :string
# custom_attributes :jsonb
# display_name :string # display_name :string
# email :string # email :string
# encrypted_password :string default(""), not null # encrypted_password :string default(""), not null
@ -112,6 +113,7 @@ class User < ApplicationRecord
self[:display_name].presence || name self[:display_name].presence || name
end end
# Used internally for Chatwoot in Chatwoot
def hmac_identifier def hmac_identifier
hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY'] hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY']
return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present? return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present?

View file

@ -4,6 +4,7 @@ json.confirmed resource.confirmed?
json.email resource.email json.email resource.email
json.available_name resource.available_name json.available_name resource.available_name
json.id resource.id json.id resource.id
json.custom_attributes resource.custom_attributes if resource.custom_attributes.present?
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

View file

@ -12,6 +12,7 @@ json.inviter_id resource.active_account_user&.inviter_id
json.name resource.name json.name resource.name
json.provider resource.provider json.provider resource.provider
json.pubsub_token resource.pubsub_token json.pubsub_token resource.pubsub_token
json.custom_attributes resource.custom_attributes if resource.custom_attributes.present?
json.role resource.active_account_user&.role json.role resource.active_account_user&.role
json.ui_settings resource.ui_settings json.ui_settings resource.ui_settings
json.uid resource.uid json.uid resource.uid

View file

@ -10,6 +10,7 @@ json.id resource.id
json.name resource.name json.name resource.name
json.provider resource.provider json.provider resource.provider
json.pubsub_token resource.pubsub_token json.pubsub_token resource.pubsub_token
json.custom_attributes resource.custom_attributes if resource.custom_attributes.present?
json.role resource.active_account_user&.role json.role resource.active_account_user&.role
json.ui_settings resource.ui_settings json.ui_settings resource.ui_settings
json.uid resource.uid json.uid resource.uid

View file

@ -0,0 +1,5 @@
class AddCustomAttributesToUser < ActiveRecord::Migration[6.1]
def change
add_column :users, :custom_attributes, :jsonb, default: {}
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.define(version: 2021_08_24_152852) do ActiveRecord::Schema.define(version: 2021_08_27_120929) 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"
@ -659,6 +659,7 @@ ActiveRecord::Schema.define(version: 2021_08_24_152852) do
t.string "pubsub_token" t.string "pubsub_token"
t.integer "availability", default: 0 t.integer "availability", default: 0
t.jsonb "ui_settings", default: {} t.jsonb "ui_settings", default: {}
t.jsonb "custom_attributes", default: {}
t.index ["email"], name: "index_users_on_email" t.index ["email"], name: "index_users_on_email"
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true

View file

@ -2,7 +2,7 @@ require 'rails_helper'
RSpec.describe 'Agents API', type: :request do RSpec.describe 'Agents API', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:admin) { create(:user, account: account, role: :administrator) } let(:admin) { create(:user, custom_attributes: { test: 'test' }, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
describe 'GET /api/v1/accounts/{account.id}/agents' do describe 'GET /api/v1/accounts/{account.id}/agents' do
@ -25,6 +25,18 @@ RSpec.describe 'Agents API', type: :request do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(JSON.parse(response.body).size).to eq(account.users.count) expect(JSON.parse(response.body).size).to eq(account.users.count)
end end
it 'returns custom fields on agents if present' do
agent.update(custom_attributes: { test: 'test' })
get "/api/v1/accounts/#{account.id}/agents",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data.first['custom_attributes']['test']).to eq('test')
end
end end
end end

View file

@ -13,7 +13,7 @@ RSpec.describe 'Profile API', type: :request do
end end
context 'when it is an authenticated user' do context 'when it is an authenticated user' do
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, custom_attributes: { test: 'test' }, role: :agent) }
it 'returns current user information' do it 'returns current user information' do
get '/api/v1/profile', get '/api/v1/profile',
@ -25,6 +25,7 @@ RSpec.describe 'Profile API', type: :request do
expect(json_response['id']).to eq(agent.id) expect(json_response['id']).to eq(agent.id)
expect(json_response['email']).to eq(agent.email) expect(json_response['email']).to eq(agent.email)
expect(json_response['access_token']).to eq(agent.access_token.token) expect(json_response['access_token']).to eq(agent.access_token.token)
expect(json_response['custom_attributes']['test']).to eq('test')
end end
end end
end end

View file

@ -104,4 +104,38 @@ RSpec.describe 'Platform Accounts API', type: :request do
end end
end end
end end
describe 'DELETE /platform/api/v1/accounts/{account_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
delete "/platform/api/v1/accounts/#{account.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
delete "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
delete "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'destroys the object' do
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
expect(DeleteObjectJob).to receive(:perform_later).with(account).once
delete "/platform/api/v1/accounts/#{account.id}",
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
end
end
end
end end

View file

@ -1,7 +1,7 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Platform Users API', type: :request do RSpec.describe 'Platform Users API', type: :request do
let!(:user) { create(:user) } let!(:user) { create(:user, custom_attributes: { test: 'test' }) }
describe 'GET /platform/api/v1/users/{user_id}' do describe 'GET /platform/api/v1/users/{user_id}' do
context 'when it is an unauthenticated platform app' do context 'when it is an unauthenticated platform app' do
@ -35,6 +35,7 @@ RSpec.describe 'Platform Users API', type: :request do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
data = JSON.parse(response.body) data = JSON.parse(response.body)
expect(data['email']).to eq(user.email) expect(data['email']).to eq(user.email)
expect(data['custom_attributes']['test']).to eq('test')
end end
end end
end end
@ -94,12 +95,14 @@ RSpec.describe 'Platform Users API', type: :request do
let(:platform_app) { create(:platform_app) } let(:platform_app) { create(:platform_app) }
it 'creates a new user and permissible for the user' do it 'creates a new user and permissible for the user' do
post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!' }, post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'Password1!',
custom_attributes: { test: 'test_create' } },
headers: { api_access_token: platform_app.access_token.token }, as: :json headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
data = JSON.parse(response.body) data = JSON.parse(response.body)
expect(data['email']).to eq('test@test.com') expect(data['email']).to eq('test@test.com')
expect(data['custom_attributes']['test']).to eq('test_create')
expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id'] expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id']
end end
@ -142,12 +145,46 @@ RSpec.describe 'Platform Users API', type: :request do
it 'updates the user' do it 'updates the user' do
create(:platform_app_permissible, platform_app: platform_app, permissible: user) create(:platform_app_permissible, platform_app: platform_app, permissible: user)
patch "/platform/api/v1/users/#{user.id}", params: { name: 'test123' }, patch "/platform/api/v1/users/#{user.id}", params: { name: 'test123', custom_attributes: { test: 'test_update' } },
headers: { api_access_token: platform_app.access_token.token }, as: :json headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
data = JSON.parse(response.body) data = JSON.parse(response.body)
expect(data['name']).to eq('test123') expect(data['name']).to eq('test123')
expect(data['custom_attributes']['test']).to eq('test_update')
end
end
end
describe 'DELETE /platform/api/v1/users/{user_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
delete "/platform/api/v1/users/#{user.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
delete "/platform/api/v1/users/#{user.id}", headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
delete "/platform/api/v1/users/#{user.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'deletes the user' do
create(:platform_app_permissible, platform_app: platform_app, permissible: user)
expect(DeleteObjectJob).to receive(:perform_later).with(user).once
delete "/platform/api/v1/users/#{user.id}",
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
end end
end end
end end

View file

@ -0,0 +1,20 @@
require 'rails_helper'
RSpec.describe DeleteObjectJob, type: :job do
subject(:job) { described_class.perform_later(account) }
let(:account) { create(:account) }
it 'enqueues the job' do
expect { job }.to have_enqueued_job(described_class)
.with(account)
.on_queue('default')
end
context 'when an object is passed to the job' do
it 'is deleted' do
described_class.perform_now(account)
expect { account.reload }.to raise_error(ActiveRecord::RecordNotFound)
end
end
end

View file

@ -9,4 +9,7 @@ properties:
password: password:
type: string type: string
description: Password must contain uppercase, lowercase letters, number and a special character description: Password must contain uppercase, lowercase letters, number and a special character
custom_attributes:
type: object
description: Custom attributes you want to associate with the user

View file

@ -19,6 +19,9 @@ properties:
enum: ['agent', 'administrator'] enum: ['agent', 'administrator']
confirmed: confirmed:
type: boolean type: boolean
custom_attributes:
type: object
description: Available for users who are created through platform APIs and has custom attributes associated.
accounts: accounts:
type: array type: array
items: items:

View file

@ -64,7 +64,7 @@ x-tagGroups:
- Teams - Teams
- Custom Filter - Custom Filter
- Reports - Reports
- name: Public - name: Client
tags: tags:
- Contacts API - Contacts API
- Conversations API - Conversations API

View file

@ -3085,6 +3085,10 @@
"confirmed": { "confirmed": {
"type": "boolean" "type": "boolean"
}, },
"custom_attributes": {
"type": "object",
"description": "Available for users who are created through platform APIs and has custom attributes associated."
},
"accounts": { "accounts": {
"type": "array", "type": "array",
"items": { "items": {
@ -3489,6 +3493,10 @@
"password": { "password": {
"type": "string", "type": "string",
"description": "Password must contain uppercase, lowercase letters, number and a special character" "description": "Password must contain uppercase, lowercase letters, number and a special character"
},
"custom_attributes": {
"type": "object",
"description": "Custom attributes you want to associate with the user"
} }
} }
}, },
@ -4173,7 +4181,7 @@
] ]
}, },
{ {
"name": "Public", "name": "Client",
"tags": [ "tags": [
"Contacts API", "Contacts API",
"Conversations API", "Conversations API",