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/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/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index daced76dd..46dde1deb 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -76,7 +76,10 @@
"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",
+ "RESET_SUCCESS": "Access token regenerated",
+ "RESET_ERROR": "Could not reset token"
},
"AUDIO_NOTIFICATIONS_SECTION": {
"TITLE": "Audio Alerts",
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue
index abf547b69..ba16a7dc0 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/AccessToken.vue
@@ -7,7 +7,7 @@ const props = defineProps({
default: '',
},
});
-const emit = defineEmits(['onCopy']);
+const emit = defineEmits(['onCopy', 'onReset']);
const inputType = ref('password');
const toggleMasked = () => {
inputType.value = inputType.value === 'password' ? 'text' : 'password';
@@ -20,6 +20,10 @@ const maskIcon = computed(() => {
const onClick = () => {
emit('onCopy', props.value);
};
+
+const onReset = () => {
+ emit('onReset');
+};
@@ -56,5 +60,15 @@ const onClick = () => {
>
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY') }}
+
+ {{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET') }}
+
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/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/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/database.yml b/config/database.yml
index 0577e3ee6..7a28adb4a 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -16,16 +16,13 @@ development:
<<: *default
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_dev') %>"
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'postgres') %>"
- password: "<%= ENV.fetch('POSTGRES_PASSWORD', '') %>"
test:
<<: *default
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_test') %>"
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'postgres') %>"
- password: "<%= ENV.fetch('POSTGRES_PASSWORD', '') %>"
production:
<<: *default
database: "<%= ENV.fetch('POSTGRES_DATABASE', 'chatwoot_production') %>"
username: "<%= ENV.fetch('POSTGRES_USERNAME', 'chatwoot_prod') %>"
- password: "<%= ENV.fetch('POSTGRES_PASSWORD', 'chatwoot_prod') %>"
diff --git a/config/routes.rb b/config/routes.rb
index 4b4db7b6d..df345b540 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -295,6 +295,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/profiles_controller_spec.rb b/spec/controllers/api/v1/profiles_controller_spec.rb
index 50404ad55..1d40e4853 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
+ json_response = response.parsed_body
+ expect(json_response['access_token']).to eq(agent.access_token.token)
+ expect(agent.access_token.token).not_to eq(old_token)
+ end
+ end
+ end
end