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 @@
+
+
+
+
+
+
+ {{ confirmHint }}
+
+
+
+
+
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 @@
@@ -38,7 +45,7 @@ const onClick = () => {
>
-
- {{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY') }}
-
+
+
+
+
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