feat: Ability to reset api_access_token (#11565)
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: iamsivin <iamsivin@gmail.com> Co-authored-by: Shivam Mishra <scm.mymail@gmail.com>
This commit is contained in:
@@ -29,6 +29,11 @@ class Api::V1::Accounts::AgentBotsController < Api::V1::Accounts::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset_access_token
|
||||||
|
@agent_bot.access_token.regenerate_token
|
||||||
|
@agent_bot.reload
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def agent_bot
|
def agent_bot
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ class Api::V1::ProfilesController < Api::BaseController
|
|||||||
head :ok
|
head :ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset_access_token
|
||||||
|
@user.access_token.regenerate_token
|
||||||
|
@user.reload
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def set_user
|
def set_user
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ class AgentBotsAPI extends ApiClient {
|
|||||||
deleteAgentBotAvatar(botId) {
|
deleteAgentBotAvatar(botId) {
|
||||||
return axios.delete(`${this.url}/${botId}/avatar`);
|
return axios.delete(`${this.url}/${botId}/avatar`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resetAccessToken(botId) {
|
||||||
|
return axios.post(`${this.url}/${botId}/reset_access_token`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AgentBotsAPI();
|
export default new AgentBotsAPI();
|
||||||
|
|||||||
@@ -102,4 +102,8 @@ export default {
|
|||||||
const urlData = endPoints('resendConfirmation');
|
const urlData = endPoints('resendConfirmation');
|
||||||
return axios.post(urlData.url);
|
return axios.post(urlData.url);
|
||||||
},
|
},
|
||||||
|
resetAccessToken() {
|
||||||
|
const urlData = endPoints('resetAccessToken');
|
||||||
|
return axios.post(urlData.url);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ const endPoints = {
|
|||||||
resendConfirmation: {
|
resendConfirmation: {
|
||||||
url: '/api/v1/profile/resend_confirmation',
|
url: '/api/v1/profile/resend_confirmation',
|
||||||
},
|
},
|
||||||
|
resetAccessToken: {
|
||||||
|
url: '/api/v1/profile/reset_access_token',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default page => {
|
export default page => {
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ describe('#AgentBotsAPI', () => {
|
|||||||
expect(AgentBotsAPI).toHaveProperty('create');
|
expect(AgentBotsAPI).toHaveProperty('create');
|
||||||
expect(AgentBotsAPI).toHaveProperty('update');
|
expect(AgentBotsAPI).toHaveProperty('update');
|
||||||
expect(AgentBotsAPI).toHaveProperty('delete');
|
expect(AgentBotsAPI).toHaveProperty('delete');
|
||||||
|
expect(AgentBotsAPI).toHaveProperty('resetAccessToken');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<script setup>
|
||||||
|
import ConfirmButton from './ConfirmButton.vue';
|
||||||
|
import { ref } from 'vue';
|
||||||
|
|
||||||
|
const count = ref(0);
|
||||||
|
|
||||||
|
const incrementCount = () => {
|
||||||
|
count.value += 1;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Story
|
||||||
|
title="Components/ConfirmButton"
|
||||||
|
:layout="{ type: 'grid', width: '400px' }"
|
||||||
|
>
|
||||||
|
<Variant title="Basic">
|
||||||
|
<div class="grid gap-2 p-4 bg-white dark:bg-slate-900">
|
||||||
|
<p>{{ count }}</p>
|
||||||
|
<ConfirmButton
|
||||||
|
label="Delete"
|
||||||
|
confirm-label="Confirm?"
|
||||||
|
@click="incrementCount"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
|
||||||
|
<Variant title="Color Change">
|
||||||
|
<div class="grid gap-2 p-4 bg-white dark:bg-slate-900">
|
||||||
|
<p>{{ count }}</p>
|
||||||
|
<ConfirmButton
|
||||||
|
label="Archive"
|
||||||
|
confirm-label="Confirm?"
|
||||||
|
color="slate"
|
||||||
|
confirm-color="amber"
|
||||||
|
@click="incrementCount"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Variant>
|
||||||
|
</Story>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
import Button from './Button.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
label: { type: [String, Number], default: '' },
|
||||||
|
confirmLabel: { type: [String, Number], default: '' },
|
||||||
|
color: { type: String, default: 'blue' },
|
||||||
|
confirmColor: { type: String, default: 'ruby' },
|
||||||
|
confirmHint: { type: String, default: '' },
|
||||||
|
variant: { type: String, default: null },
|
||||||
|
size: { type: String, default: null },
|
||||||
|
justify: { type: String, default: null },
|
||||||
|
icon: { type: [String, Object, Function], default: '' },
|
||||||
|
trailingIcon: { type: Boolean, default: false },
|
||||||
|
isLoading: { type: Boolean, default: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['click']);
|
||||||
|
|
||||||
|
const isConfirmMode = ref(false);
|
||||||
|
const isClicked = ref(false);
|
||||||
|
|
||||||
|
const currentLabel = computed(() => {
|
||||||
|
return isConfirmMode.value ? props.confirmLabel : props.label;
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentColor = computed(() => {
|
||||||
|
return isConfirmMode.value ? props.confirmColor : props.color;
|
||||||
|
});
|
||||||
|
const resetConfirmMode = () => {
|
||||||
|
isConfirmMode.value = false;
|
||||||
|
isClicked.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (!isConfirmMode.value) {
|
||||||
|
isConfirmMode.value = true;
|
||||||
|
} else {
|
||||||
|
isClicked.value = true;
|
||||||
|
emit('click');
|
||||||
|
setTimeout(resetConfirmMode, 400);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="relative"
|
||||||
|
:class="{
|
||||||
|
'animate-bounce-complete': isClicked,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
:label="currentLabel"
|
||||||
|
:color="currentColor"
|
||||||
|
:variant="variant"
|
||||||
|
:size="size"
|
||||||
|
:justify="justify"
|
||||||
|
:icon="icon"
|
||||||
|
:trailing-icon="trailingIcon"
|
||||||
|
:is-loading="isLoading"
|
||||||
|
@click="handleClick"
|
||||||
|
@blur="resetConfirmMode"
|
||||||
|
>
|
||||||
|
<template v-if="$slots.default" #default>
|
||||||
|
<slot />
|
||||||
|
</template>
|
||||||
|
<template v-if="$slots.icon" #icon>
|
||||||
|
<slot name="icon" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
<div
|
||||||
|
v-if="isConfirmMode && confirmHint"
|
||||||
|
class="absolute mt-1 w-full text-[10px] text-center text-n-slate-10"
|
||||||
|
>
|
||||||
|
{{ confirmHint }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@keyframes bounce-complete {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-bounce-complete {
|
||||||
|
animation: bounce-complete 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -62,7 +62,9 @@
|
|||||||
"ACCESS_TOKEN": {
|
"ACCESS_TOKEN": {
|
||||||
"TITLE": "Access Token",
|
"TITLE": "Access Token",
|
||||||
"DESCRIPTION": "Copy the access token and save it securely",
|
"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": {
|
"FORM": {
|
||||||
"AVATAR": {
|
"AVATAR": {
|
||||||
|
|||||||
@@ -76,7 +76,12 @@
|
|||||||
"ACCESS_TOKEN": {
|
"ACCESS_TOKEN": {
|
||||||
"TITLE": "Access Token",
|
"TITLE": "Access Token",
|
||||||
"NOTE": "This token can be used if you are building an API based integration",
|
"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": {
|
"AUDIO_NOTIFICATIONS_SECTION": {
|
||||||
"TITLE": "Audio Alerts",
|
"TITLE": "Audio Alerts",
|
||||||
|
|||||||
@@ -228,7 +228,20 @@ const initializeForm = () => {
|
|||||||
|
|
||||||
const onCopyToken = async value => {
|
const onCopyToken = async value => {
|
||||||
await copyTextToClipboard(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 = () => {
|
const closeModal = () => {
|
||||||
@@ -312,7 +325,18 @@ defineExpose({ dialogRef });
|
|||||||
>
|
>
|
||||||
{{ $t('AGENT_BOTS.ACCESS_TOKEN.TITLE') }}
|
{{ $t('AGENT_BOTS.ACCESS_TOKEN.TITLE') }}
|
||||||
</label>
|
</label>
|
||||||
<AccessToken :value="accessToken" @on-copy="onCopyToken" />
|
<AccessToken
|
||||||
|
v-if="type === MODAL_TYPES.EDIT"
|
||||||
|
:value="accessToken"
|
||||||
|
@on-copy="onCopyToken"
|
||||||
|
@on-reset="onResetToken"
|
||||||
|
/>
|
||||||
|
<AccessToken
|
||||||
|
v-else
|
||||||
|
:value="accessToken"
|
||||||
|
:show-reset-button="false"
|
||||||
|
@on-copy="onCopyToken"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end w-full gap-2 px-0 py-2">
|
<div class="flex items-center justify-end w-full gap-2 px-0 py-2">
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from 'vue';
|
||||||
import FormButton from 'v3/components/Form/Button.vue';
|
import NextButton from 'dashboard/components-next/button/Button.vue';
|
||||||
|
import ConfirmButton from 'dashboard/components-next/button/ConfirmButton.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
value: {
|
value: { type: String, default: '' },
|
||||||
type: String,
|
showResetButton: { type: Boolean, default: true },
|
||||||
default: '',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const emit = defineEmits(['onCopy']);
|
|
||||||
|
const emit = defineEmits(['onCopy', 'onReset']);
|
||||||
|
|
||||||
const inputType = ref('password');
|
const inputType = ref('password');
|
||||||
|
|
||||||
const toggleMasked = () => {
|
const toggleMasked = () => {
|
||||||
inputType.value = inputType.value === 'password' ? 'text' : 'password';
|
inputType.value = inputType.value === 'password' ? 'text' : 'password';
|
||||||
};
|
};
|
||||||
@@ -20,6 +23,10 @@ const maskIcon = computed(() => {
|
|||||||
const onClick = () => {
|
const onClick = () => {
|
||||||
emit('onCopy', props.value);
|
emit('onCopy', props.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onReset = () => {
|
||||||
|
emit('onReset');
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -38,7 +45,7 @@ const onClick = () => {
|
|||||||
>
|
>
|
||||||
<template #masked>
|
<template #masked>
|
||||||
<button
|
<button
|
||||||
class="absolute top-1.5 ltr:right-0.5 rtl:left-0.5"
|
class="absolute top-0 bottom-0 ltr:right-0.5 rtl:left-0.5"
|
||||||
type="button"
|
type="button"
|
||||||
@click="toggleMasked"
|
@click="toggleMasked"
|
||||||
>
|
>
|
||||||
@@ -46,15 +53,28 @@ const onClick = () => {
|
|||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
</woot-input>
|
</woot-input>
|
||||||
<FormButton
|
<div class="flex flex-row gap-2">
|
||||||
type="button"
|
<NextButton
|
||||||
size="large"
|
:label="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY')"
|
||||||
icon="text-copy"
|
slate
|
||||||
variant="outline"
|
outline
|
||||||
color-scheme="secondary"
|
type="button"
|
||||||
@click="onClick"
|
icon="i-lucide-copy"
|
||||||
>
|
class="rounded-xl"
|
||||||
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.COPY') }}
|
@click="onClick"
|
||||||
</FormButton>
|
/>
|
||||||
|
<ConfirmButton
|
||||||
|
v-if="showResetButton"
|
||||||
|
:label="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.RESET')"
|
||||||
|
:confirm-label="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.CONFIRM_RESET')"
|
||||||
|
:confirm-hint="$t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.CONFIRM_HINT')"
|
||||||
|
color="slate"
|
||||||
|
confirm-color="ruby"
|
||||||
|
variant="outline"
|
||||||
|
icon="i-lucide-key-round"
|
||||||
|
class="rounded-xl"
|
||||||
|
@click="onReset"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -181,6 +181,14 @@ export default {
|
|||||||
await copyTextToClipboard(value);
|
await copyTextToClipboard(value);
|
||||||
useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
|
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'));
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
@@ -281,7 +289,11 @@ export default {
|
|||||||
)
|
)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<AccessToken :value="currentUser.access_token" @on-copy="onCopyToken" />
|
<AccessToken
|
||||||
|
:value="currentUser.access_token"
|
||||||
|
@on-copy="onCopyToken"
|
||||||
|
@on-reset="resetAccessToken"
|
||||||
|
/>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -172,6 +172,17 @@ export const actions = {
|
|||||||
commit(types.SET_AGENT_BOT_UI_FLAG, { isDisconnecting: false });
|
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 = {
|
export const mutations = {
|
||||||
|
|||||||
@@ -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 () => {
|
resendConfirmation: async () => {
|
||||||
try {
|
try {
|
||||||
await authAPI.resendConfirmation();
|
await authAPI.resendConfirmation();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,4 +22,8 @@ class AgentBotPolicy < ApplicationPolicy
|
|||||||
def avatar?
|
def avatar?
|
||||||
@account_user.administrator?
|
@account_user.administrator?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def reset_access_token?
|
||||||
|
@account_user.administrator?
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
json.partial! 'api/v1/models/agent_bot', formats: [:json], resource: AgentBotPresenter.new(@agent_bot)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
json.partial! 'api/v1/models/user', formats: [:json], resource: @user
|
||||||
@@ -67,6 +67,7 @@ Rails.application.routes.draw do
|
|||||||
end
|
end
|
||||||
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
|
resources :agent_bots, only: [:index, :create, :show, :update, :destroy] do
|
||||||
delete :avatar, on: :member
|
delete :avatar, on: :member
|
||||||
|
post :reset_access_token, on: :member
|
||||||
end
|
end
|
||||||
resources :contact_inboxes, only: [] do
|
resources :contact_inboxes, only: [] do
|
||||||
collection do
|
collection do
|
||||||
@@ -296,6 +297,7 @@ Rails.application.routes.draw do
|
|||||||
post :auto_offline
|
post :auto_offline
|
||||||
put :set_active_account
|
put :set_active_account
|
||||||
post :resend_confirmation
|
post :resend_confirmation
|
||||||
|
post :reset_access_token
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -262,4 +262,55 @@ RSpec.describe 'Agent Bot API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
@@ -296,4 +296,32 @@ RSpec.describe 'Profile API', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
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
|
end
|
||||||
|
|||||||
Reference in New Issue
Block a user