From ad83d1bb71434e3f5fa91890d1fdd67314108bdb Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 2 Sep 2021 18:29:45 +0530 Subject: [PATCH] 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 --- .../platform/api/v1/accounts_controller.rb | 2 +- .../platform/api/v1/users_controller.rb | 18 ++++++-- app/jobs/delete_object_job.rb | 7 +++ app/models/user.rb | 2 + app/views/api/v1/models/_agent.json.jbuilder | 1 + app/views/api/v1/models/_user.json.jbuilder | 1 + .../api/v1/models/_user.json.jbuilder | 1 + ...827120929_add_custom_attributes_to_user.rb | 5 +++ db/schema.rb | 3 +- .../api/v1/accounts/agents_controller_spec.rb | 14 +++++- .../api/v1/profiles_controller_spec.rb | 3 +- .../api/v1/accounts_controller_spec.rb | 34 +++++++++++++++ .../platform/api/v1/users_controller_spec.rb | 43 +++++++++++++++++-- spec/jobs/delete_object_job_spec.rb | 20 +++++++++ .../request/user/create_update_payload.yml | 3 ++ swagger/definitions/resource/user.yml | 3 ++ swagger/index.yml | 2 +- swagger/swagger.json | 10 ++++- 18 files changed, 160 insertions(+), 12 deletions(-) create mode 100644 app/jobs/delete_object_job.rb create mode 100644 db/migrate/20210827120929_add_custom_attributes_to_user.rb create mode 100644 spec/jobs/delete_object_job_spec.rb diff --git a/app/controllers/platform/api/v1/accounts_controller.rb b/app/controllers/platform/api/v1/accounts_controller.rb index 24b39aa3e..a037f4e2f 100644 --- a/app/controllers/platform/api/v1/accounts_controller.rb +++ b/app/controllers/platform/api/v1/accounts_controller.rb @@ -16,7 +16,7 @@ class Platform::Api::V1::AccountsController < PlatformController end def destroy - # TODO: obfusicate account + DeleteObjectJob.perform_later(@resource) head :ok end diff --git a/app/controllers/platform/api/v1/users_controller.rb b/app/controllers/platform/api/v1/users_controller.rb index b6e9237b9..ccec3b2bc 100644 --- a/app/controllers/platform/api/v1/users_controller.rb +++ b/app/controllers/platform/api/v1/users_controller.rb @@ -19,21 +19,33 @@ class Platform::Api::V1::UsersController < PlatformController def show; end def update - @resource.update!(user_params) + @resource.assign_attributes(user_update_params) + @resource.save! end def destroy - # TODO: obfusicate user + DeleteObjectJob.perform_later(@resource) head :ok end 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 @resource = User.find(params[:id]) end def user_params - params.permit(:name, :email, :password) + params.permit(:name, :email, :password, custom_attributes: {}) end end diff --git a/app/jobs/delete_object_job.rb b/app/jobs/delete_object_job.rb new file mode 100644 index 000000000..e478cd374 --- /dev/null +++ b/app/jobs/delete_object_job.rb @@ -0,0 +1,7 @@ +class DeleteObjectJob < ApplicationJob + queue_as :default + + def perform(object) + object.destroy! + end +end diff --git a/app/models/user.rb b/app/models/user.rb index c6342695f..bc0a2d20f 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ # confirmed_at :datetime # current_sign_in_at :datetime # current_sign_in_ip :string +# custom_attributes :jsonb # display_name :string # email :string # encrypted_password :string default(""), not null @@ -112,6 +113,7 @@ class User < ApplicationRecord self[:display_name].presence || name end + # Used internally for Chatwoot in Chatwoot def hmac_identifier hmac_key = GlobalConfig.get('CHATWOOT_INBOX_HMAC_KEY')['CHATWOOT_INBOX_HMAC_KEY'] return OpenSSL::HMAC.hexdigest('sha256', hmac_key, email) if hmac_key.present? diff --git a/app/views/api/v1/models/_agent.json.jbuilder b/app/views/api/v1/models/_agent.json.jbuilder index 6a0932d7a..59a31603d 100644 --- a/app/views/api/v1/models/_agent.json.jbuilder +++ b/app/views/api/v1/models/_agent.json.jbuilder @@ -4,6 +4,7 @@ json.confirmed resource.confirmed? json.email resource.email json.available_name resource.available_name json.id resource.id +json.custom_attributes resource.custom_attributes if resource.custom_attributes.present? json.name resource.name json.role resource.role json.thumbnail resource.avatar_url diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder index bc7392bf0..5c0cdeec9 100644 --- a/app/views/api/v1/models/_user.json.jbuilder +++ b/app/views/api/v1/models/_user.json.jbuilder @@ -12,6 +12,7 @@ json.inviter_id resource.active_account_user&.inviter_id json.name resource.name json.provider resource.provider 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.ui_settings resource.ui_settings json.uid resource.uid diff --git a/app/views/platform/api/v1/models/_user.json.jbuilder b/app/views/platform/api/v1/models/_user.json.jbuilder index 8f7922aad..1ec708b8d 100644 --- a/app/views/platform/api/v1/models/_user.json.jbuilder +++ b/app/views/platform/api/v1/models/_user.json.jbuilder @@ -10,6 +10,7 @@ json.id resource.id json.name resource.name json.provider resource.provider 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.ui_settings resource.ui_settings json.uid resource.uid diff --git a/db/migrate/20210827120929_add_custom_attributes_to_user.rb b/db/migrate/20210827120929_add_custom_attributes_to_user.rb new file mode 100644 index 000000000..4eb709284 --- /dev/null +++ b/db/migrate/20210827120929_add_custom_attributes_to_user.rb @@ -0,0 +1,5 @@ +class AddCustomAttributesToUser < ActiveRecord::Migration[6.1] + def change + add_column :users, :custom_attributes, :jsonb, default: {} + end +end diff --git a/db/schema.rb b/db/schema.rb index a844bda7f..86bb50653 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # 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 enable_extension "pg_stat_statements" @@ -659,6 +659,7 @@ ActiveRecord::Schema.define(version: 2021_08_24_152852) do t.string "pubsub_token" t.integer "availability", default: 0 t.jsonb "ui_settings", default: {} + t.jsonb "custom_attributes", default: {} t.index ["email"], name: "index_users_on_email" 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 diff --git a/spec/controllers/api/v1/accounts/agents_controller_spec.rb b/spec/controllers/api/v1/accounts/agents_controller_spec.rb index 832840b34..872b14669 100644 --- a/spec/controllers/api/v1/accounts/agents_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/agents_controller_spec.rb @@ -2,7 +2,7 @@ require 'rails_helper' RSpec.describe 'Agents API', type: :request do 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) } 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(JSON.parse(response.body).size).to eq(account.users.count) 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 diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb index c73194fec..3247b5f83 100644 --- a/spec/controllers/api/v1/profiles_controller_spec.rb +++ b/spec/controllers/api/v1/profiles_controller_spec.rb @@ -13,7 +13,7 @@ RSpec.describe 'Profile API', type: :request do end 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 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['email']).to eq(agent.email) expect(json_response['access_token']).to eq(agent.access_token.token) + expect(json_response['custom_attributes']['test']).to eq('test') end end end diff --git a/spec/controllers/platform/api/v1/accounts_controller_spec.rb b/spec/controllers/platform/api/v1/accounts_controller_spec.rb index 0b1daf622..e81be05ac 100644 --- a/spec/controllers/platform/api/v1/accounts_controller_spec.rb +++ b/spec/controllers/platform/api/v1/accounts_controller_spec.rb @@ -104,4 +104,38 @@ RSpec.describe 'Platform Accounts API', type: :request do 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 diff --git a/spec/controllers/platform/api/v1/users_controller_spec.rb b/spec/controllers/platform/api/v1/users_controller_spec.rb index e25f3cb42..f9d982cbc 100644 --- a/spec/controllers/platform/api/v1/users_controller_spec.rb +++ b/spec/controllers/platform/api/v1/users_controller_spec.rb @@ -1,7 +1,7 @@ require 'rails_helper' 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 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) data = JSON.parse(response.body) expect(data['email']).to eq(user.email) + expect(data['custom_attributes']['test']).to eq('test') end end end @@ -94,12 +95,14 @@ RSpec.describe 'Platform Users API', type: :request do let(:platform_app) { create(:platform_app) } 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 expect(response).to have_http_status(:success) data = JSON.parse(response.body) 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'] end @@ -142,12 +145,46 @@ RSpec.describe 'Platform Users API', type: :request do it 'updates the user' do 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 expect(response).to have_http_status(:success) data = JSON.parse(response.body) 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 diff --git a/spec/jobs/delete_object_job_spec.rb b/spec/jobs/delete_object_job_spec.rb new file mode 100644 index 000000000..7ed623bd4 --- /dev/null +++ b/spec/jobs/delete_object_job_spec.rb @@ -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 diff --git a/swagger/definitions/request/user/create_update_payload.yml b/swagger/definitions/request/user/create_update_payload.yml index a7745f18f..e14a25317 100644 --- a/swagger/definitions/request/user/create_update_payload.yml +++ b/swagger/definitions/request/user/create_update_payload.yml @@ -9,4 +9,7 @@ properties: password: type: string 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 diff --git a/swagger/definitions/resource/user.yml b/swagger/definitions/resource/user.yml index 04733b6b0..c06cbab8e 100644 --- a/swagger/definitions/resource/user.yml +++ b/swagger/definitions/resource/user.yml @@ -19,6 +19,9 @@ properties: enum: ['agent', 'administrator'] confirmed: type: boolean + custom_attributes: + type: object + description: Available for users who are created through platform APIs and has custom attributes associated. accounts: type: array items: diff --git a/swagger/index.yml b/swagger/index.yml index 565d66f94..2d1ef704a 100644 --- a/swagger/index.yml +++ b/swagger/index.yml @@ -64,7 +64,7 @@ x-tagGroups: - Teams - Custom Filter - Reports - - name: Public + - name: Client tags: - Contacts API - Conversations API diff --git a/swagger/swagger.json b/swagger/swagger.json index 102e7f8d5..94c472908 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -3085,6 +3085,10 @@ "confirmed": { "type": "boolean" }, + "custom_attributes": { + "type": "object", + "description": "Available for users who are created through platform APIs and has custom attributes associated." + }, "accounts": { "type": "array", "items": { @@ -3489,6 +3493,10 @@ "password": { "type": "string", "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": [ "Contacts API", "Conversations API",