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:
parent
a60a33679f
commit
ad83d1bb71
18 changed files with 160 additions and 12 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
7
app/jobs/delete_object_job.rb
Normal file
7
app/jobs/delete_object_job.rb
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
class DeleteObjectJob < ApplicationJob
|
||||||
|
queue_as :default
|
||||||
|
|
||||||
|
def perform(object)
|
||||||
|
object.destroy!
|
||||||
|
end
|
||||||
|
end
|
|
@ -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?
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddCustomAttributesToUser < ActiveRecord::Migration[6.1]
|
||||||
|
def change
|
||||||
|
add_column :users, :custom_attributes, :jsonb, default: {}
|
||||||
|
end
|
||||||
|
end
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
20
spec/jobs/delete_object_job_spec.rb
Normal file
20
spec/jobs/delete_object_job_spec.rb
Normal 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
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue