diff --git a/app/controllers/api/v1/accounts/agent_bots_controller.rb b/app/controllers/api/v1/accounts/agent_bots_controller.rb index 1422beea1..64c35d33d 100644 --- a/app/controllers/api/v1/accounts/agent_bots_controller.rb +++ b/app/controllers/api/v1/accounts/agent_bots_controller.rb @@ -29,6 +29,11 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController head :ok end + def reset_access_token + @agent_bot.access_token.regenerate_token + @agent_bot.reload + end + private def agent_bot diff --git a/app/controllers/api/v1/profiles_controller.rb b/app/controllers/api/v1/profiles_controller.rb index ae1a1fe30..141253d0d 100644 --- a/app/controllers/api/v1/profiles_controller.rb +++ b/app/controllers/api/v1/profiles_controller.rb @@ -38,6 +38,11 @@ class Api::V1::ProfilesController < Api::BaseController head :ok end + def reset_access_token + @user.access_token.regenerate_token + @user.reload + end + private def set_user diff --git a/app/javascript/dashboard/api/agentBots.js b/app/javascript/dashboard/api/agentBots.js index 6e59f38d3..de887f415 100644 --- a/app/javascript/dashboard/api/agentBots.js +++ b/app/javascript/dashboard/api/agentBots.js @@ -21,6 +21,10 @@ class AgentBotsAPI extends ApiClient { deleteAgentBotAvatar(botId) { return axios.delete(`${this.url}/${botId}/avatar`); } + + resetAccessToken(botId) { + return axios.post(`${this.url}/${botId}/reset_access_token`); + } } export default new AgentBotsAPI(); diff --git a/app/javascript/dashboard/api/auth.js b/app/javascript/dashboard/api/auth.js index dde817866..75e7e2953 100644 --- a/app/javascript/dashboard/api/auth.js +++ b/app/javascript/dashboard/api/auth.js @@ -102,4 +102,8 @@ export default { const urlData = endPoints('resendConfirmation'); return axios.post(urlData.url); }, + resetAccessToken() { + const urlData = endPoints('resetAccessToken'); + return axios.post(urlData.url); + }, }; diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index 31337b7fc..5409aac60 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -51,6 +51,9 @@ const endPoints = { resendConfirmation: { url: '/api/v1/profile/resend_confirmation', }, + resetAccessToken: { + url: '/api/v1/profile/reset_access_token', + }, }; export default page => { diff --git a/app/javascript/dashboard/api/specs/agentBots.spec.js b/app/javascript/dashboard/api/specs/agentBots.spec.js index c89dbfdf5..bf57804c0 100644 --- a/app/javascript/dashboard/api/specs/agentBots.spec.js +++ b/app/javascript/dashboard/api/specs/agentBots.spec.js @@ -9,5 +9,6 @@ describe('#AgentBotsAPI', () => { expect(AgentBotsAPI).toHaveProperty('create'); expect(AgentBotsAPI).toHaveProperty('update'); expect(AgentBotsAPI).toHaveProperty('delete'); + expect(AgentBotsAPI).toHaveProperty('resetAccessToken'); }); }); diff --git a/app/javascript/dashboard/components-next/button/ConfirmButton.story.vue b/app/javascript/dashboard/components-next/button/ConfirmButton.story.vue new file mode 100644 index 000000000..673661a74 --- /dev/null +++ b/app/javascript/dashboard/components-next/button/ConfirmButton.story.vue @@ -0,0 +1,41 @@ + + + diff --git a/app/javascript/dashboard/components-next/button/ConfirmButton.vue b/app/javascript/dashboard/components-next/button/ConfirmButton.vue new file mode 100644 index 000000000..854d5d452 --- /dev/null +++ b/app/javascript/dashboard/components-next/button/ConfirmButton.vue @@ -0,0 +1,99 @@ + + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/agentBots.json b/app/javascript/dashboard/i18n/locale/en/agentBots.json index 194cfb999..d3a0bb991 100644 --- a/app/javascript/dashboard/i18n/locale/en/agentBots.json +++ b/app/javascript/dashboard/i18n/locale/en/agentBots.json @@ -62,7 +62,9 @@ "ACCESS_TOKEN": { "TITLE": "Access Token", "DESCRIPTION": "Copy the access token and save it securely", - "COPY_SUCCESSFUL": "Access token copied to clipboard" + "COPY_SUCCESSFUL": "Access token copied to clipboard", + "RESET_SUCCESS": "Access token regenerated successfully", + "RESET_ERROR": "Unable to regenerate access token. Please try again" }, "FORM": { "AVATAR": { diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index daced76dd..81b9c8a78 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -76,7 +76,12 @@ "ACCESS_TOKEN": { "TITLE": "Access Token", "NOTE": "This token can be used if you are building an API based integration", - "COPY": "Copy" + "COPY": "Copy", + "RESET": "Reset", + "CONFIRM_RESET": "Are you sure?", + "CONFIRM_HINT": "Click again to confirm", + "RESET_SUCCESS": "Access token regenerated successfully", + "RESET_ERROR": "Unable to regenerate access token. Please try again" }, "AUDIO_NOTIFICATIONS_SECTION": { "TITLE": "Audio Alerts", diff --git a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotModal.vue b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotModal.vue index 97629992a..be4deb337 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotModal.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/agentBots/components/AgentBotModal.vue @@ -228,7 +228,20 @@ const initializeForm = () => { const onCopyToken = async value => { await copyTextToClipboard(value); - useAlert(t('COMPONENTS.CODE.COPY_SUCCESSFUL')); + useAlert(t('AGENT_BOTS.ACCESS_TOKEN.COPY_SUCCESSFUL')); +}; + +const onResetToken = async () => { + const response = await store.dispatch( + 'agentBots/resetAccessToken', + props.selectedBot.id + ); + if (response) { + accessToken.value = response.access_token; + useAlert(t('AGENT_BOTS.ACCESS_TOKEN.RESET_SUCCESS')); + } else { + useAlert(t('AGENT_BOTS.ACCESS_TOKEN.RESET_ERROR')); + } }; const closeModal = () => { @@ -312,7 +325,18 @@ defineExpose({ dialogRef }); > {{ $t('AGENT_BOTS.ACCESS_TOKEN.TITLE') }} - + +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue index abf547b69..5b0b43fac 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue @@ -1,14 +1,17 @@ diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index 45a060e05..eedcd21f1 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -181,6 +181,14 @@ export default { await copyTextToClipboard(value); useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL')); }, + async resetAccessToken() { + const success = await this.$store.dispatch('resetAccessToken'); + if (success) { + useAlert(this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET_SUCCESS')); + } else { + useAlert(this.$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET_ERROR')); + } + }, }, }; @@ -281,7 +289,11 @@ export default { ) " > - +
diff --git a/app/javascript/dashboard/store/modules/agentBots.js b/app/javascript/dashboard/store/modules/agentBots.js index 3e9931057..bd7bff5f0 100644 --- a/app/javascript/dashboard/store/modules/agentBots.js +++ b/app/javascript/dashboard/store/modules/agentBots.js @@ -172,6 +172,17 @@ export const actions = { commit(types.SET_AGENT_BOT_UI_FLAG, { isDisconnecting: false }); } }, + + resetAccessToken: async ({ commit }, botId) => { + try { + const response = await AgentBotsAPI.resetAccessToken(botId); + commit(types.EDIT_AGENT_BOT, response.data); + return response.data; + } catch (error) { + throwErrorMessage(error); + return null; + } + }, }; export const mutations = { diff --git a/app/javascript/dashboard/store/modules/auth.js b/app/javascript/dashboard/store/modules/auth.js index b790b2a80..f329fb009 100644 --- a/app/javascript/dashboard/store/modules/auth.js +++ b/app/javascript/dashboard/store/modules/auth.js @@ -213,6 +213,16 @@ export const actions = { } }, + resetAccessToken: async ({ commit }) => { + try { + const response = await authAPI.resetAccessToken(); + commit(types.SET_CURRENT_USER, response.data); + return true; + } catch (error) { + return false; + } + }, + resendConfirmation: async () => { try { await authAPI.resendConfirmation(); diff --git a/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js b/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js index b2fa47313..168c8f78c 100644 --- a/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js +++ b/app/javascript/dashboard/store/modules/specs/agentBots/agentBots.spec.js @@ -170,4 +170,21 @@ describe('#actions', () => { ]); }); }); + describe('#resetAccessToken', () => { + it('sends correct actions if API is success', async () => { + const mockResponse = { + data: { ...agentBotRecords[0], access_token: 'new_token_123' }, + }; + axios.post.mockResolvedValue(mockResponse); + const result = await actions.resetAccessToken( + { commit }, + agentBotRecords[0].id + ); + + expect(commit.mock.calls).toEqual([ + [types.EDIT_AGENT_BOT, mockResponse.data], + ]); + expect(result).toBe(mockResponse.data); + }); + }); }); diff --git a/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js index 3de56b1fe..b5dfebe26 100644 --- a/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/auth/actions.spec.js @@ -228,4 +228,20 @@ describe('#actions', () => { ); }); }); + + describe('#resetAccessToken', () => { + it('sends correct actions if API is success', async () => { + const mockResponse = { + data: { id: 1, name: 'John', access_token: 'new_token_123' }, + headers: { expiry: 581842904 }, + }; + axios.post.mockResolvedValue(mockResponse); + const result = await actions.resetAccessToken({ commit }); + + expect(commit.mock.calls).toEqual([ + [types.SET_CURRENT_USER, mockResponse.data], + ]); + expect(result).toBe(true); + }); + }); }); diff --git a/app/policies/agent_bot_policy.rb b/app/policies/agent_bot_policy.rb index 75c91dbf9..7461f6b2d 100644 --- a/app/policies/agent_bot_policy.rb +++ b/app/policies/agent_bot_policy.rb @@ -22,4 +22,8 @@ class AgentBotPolicy < ApplicationPolicy def avatar? @account_user.administrator? end + + def reset_access_token? + @account_user.administrator? + end end diff --git a/app/views/api/v1/accounts/agent_bots/reset_access_token.json.jbuilder b/app/views/api/v1/accounts/agent_bots/reset_access_token.json.jbuilder new file mode 100644 index 000000000..f647ac383 --- /dev/null +++ b/app/views/api/v1/accounts/agent_bots/reset_access_token.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/models/agent_bot', formats: [:json], resource: AgentBotPresenter.new(@agent_bot) diff --git a/app/views/api/v1/profiles/reset_access_token.json.jbuilder b/app/views/api/v1/profiles/reset_access_token.json.jbuilder new file mode 100644 index 000000000..0a4b4f9fa --- /dev/null +++ b/app/views/api/v1/profiles/reset_access_token.json.jbuilder @@ -0,0 +1 @@ +json.partial! 'api/v1/models/user', formats: [:json], resource: @user diff --git a/config/routes.rb b/config/routes.rb index d1705d605..9841c7103 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -67,6 +67,7 @@ Rails.application.routes.draw do end resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do delete :avatar, on: :member + post :reset_access_token, on: :member end resources :contact_inboxes, only: [] do collection do @@ -296,6 +297,7 @@ Rails.application.routes.draw do post :auto_offline put :set_active_account post :resend_confirmation + post :reset_access_token end end diff --git a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb index b5cdff018..61fcf30ac 100644 --- a/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/agent_bots_controller_spec.rb @@ -262,4 +262,55 @@ RSpec.describe 'Agent Bot API', type: :request do end end end + + describe 'POST /api/v1/accounts/{account.id}/agent_bots/:id/reset_access_token' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/reset_access_token" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + it 'regenerates the access token when administrator' do + old_token = agent_bot.access_token.token + + post "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/reset_access_token", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + agent_bot.reload + expect(agent_bot.access_token.token).not_to eq(old_token) + json_response = response.parsed_body + expect(json_response['access_token']).to eq(agent_bot.access_token.token) + end + + it 'would not reset the access token when agent' do + old_token = agent_bot.access_token.token + + post "/api/v1/accounts/#{account.id}/agent_bots/#{agent_bot.id}/reset_access_token", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:unauthorized) + agent_bot.reload + expect(agent_bot.access_token.token).to eq(old_token) + end + + it 'would not reset access token for a global agent bot' do + global_bot = create(:agent_bot) + old_token = global_bot.access_token.token + + post "/api/v1/accounts/#{account.id}/agent_bots/#{global_bot.id}/reset_access_token", + headers: admin.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:not_found) + global_bot.reload + expect(global_bot.access_token.token).to eq(old_token) + end + end + end end diff --git a/spec/controllers/api/v1/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb index 50404ad55..8af9e30c0 100644 --- a/spec/controllers/api/v1/profiles_controller_spec.rb +++ b/spec/controllers/api/v1/profiles_controller_spec.rb @@ -296,4 +296,32 @@ RSpec.describe 'Profile API', type: :request do end end end + + describe 'POST /api/v1/profile/reset_access_token' do + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post '/api/v1/profile/reset_access_token' + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'regenerates the access token' do + old_token = agent.access_token.token + + post '/api/v1/profile/reset_access_token', + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + agent.reload + expect(agent.access_token.token).not_to eq(old_token) + json_response = response.parsed_body + expect(json_response['access_token']).to eq(agent.access_token.token) + end + end + end end