From c816e6b33088514b276673ebdb8f0429cc2f5b38 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 25 May 2026 16:16:59 +0530 Subject: [PATCH] feat: add token list --- .../dashboard/helper/featureHelper.js | 3 + .../dashboard/i18n/locale/en/settings.json | 17 +++ .../account/components/SectionLayout.vue | 1 + .../dashboard/settings/profile/Index.vue | 123 ++++++++++++--- .../dashboard/settings/profile/TokenList.vue | 141 ++++++++++++++++++ 5 files changed, 264 insertions(+), 21 deletions(-) create mode 100644 app/javascript/dashboard/routes/dashboard/settings/profile/TokenList.vue diff --git a/app/javascript/dashboard/helper/featureHelper.js b/app/javascript/dashboard/helper/featureHelper.js index c90ec15db..34d819134 100644 --- a/app/javascript/dashboard/helper/featureHelper.js +++ b/app/javascript/dashboard/helper/featureHelper.js @@ -20,6 +20,9 @@ const FEATURE_HELP_URLS = { billing: 'https://chwt.app/pricing', saml: 'https://chwt.app/hc/saml', captain_billing: 'https://chwt.app/hc/captain_billing', + access_token_api: + 'https://developers.chatwoot.com/api-reference/introduction', + chatwoot_cli: 'https://developers.chatwoot.com/cli', }; export function getHelpUrlForFeature(featureName) { diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index 6a3d2c1fa..46b8e19be 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -92,8 +92,25 @@ "COPY": "Copy", "RESET": "Reset", "GENERATE": "Generate", + "CREATE": "Create token", "CONFIRM_RESET": "Are you sure?", "CONFIRM_HINT": "Click again to confirm", + "TABLE": { + "TOKEN": "Token", + "PERMISSION": "Permission" + }, + "EMPTY": { + "TITLE": "No tokens yet", + "DESCRIPTION": "Create your first access token to start building integrations against the API." + }, + "SCOPES": { + "FULL_LABEL": "Full access", + "READ_ONLY_LABEL": "Read only" + }, + "LINKS": { + "API_REFERENCE": "API Reference", + "CLI": "Chatwoot CLI" + }, "RESET_SUCCESS": "Access token regenerated successfully", "RESET_ERROR": "Unable to regenerate access token. Please try again", "READ_ONLY_TITLE": "Read-only Access Token", diff --git a/app/javascript/dashboard/routes/dashboard/settings/account/components/SectionLayout.vue b/app/javascript/dashboard/routes/dashboard/settings/account/components/SectionLayout.vue index a54759971..9ee403c3e 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/account/components/SectionLayout.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/account/components/SectionLayout.vue @@ -45,6 +45,7 @@ const { t } = useI18n(); > {{ description }}

+
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue index 6efceb64a..301b279a5 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/Index.vue @@ -8,6 +8,10 @@ import { clearCookiesOnLogout } from 'dashboard/store/utils/api.js'; import { copyTextToClipboard } from 'shared/helpers/clipboard'; import { parseAPIErrorResponse } from 'dashboard/store/utils/api'; import { parseBoolean } from '@chatwoot/utils'; +import { OnClickOutside } from '@vueuse/components'; +import { getHelpUrlForFeature } from 'dashboard/helper/featureHelper'; +import NextButton from 'dashboard/components-next/button/Button.vue'; +import DropdownMenu from 'dashboard/components-next/dropdown-menu/DropdownMenu.vue'; import UserProfilePicture from './UserProfilePicture.vue'; import UserBasicDetails from './UserBasicDetails.vue'; import MessageSignature from './MessageSignature.vue'; @@ -18,7 +22,7 @@ import NotificationPreferences from './NotificationPreferences.vue'; import AudioNotifications from './AudioNotifications.vue'; import SectionLayout from '../account/components/SectionLayout.vue'; import BaseSettingsHeader from '../components/BaseSettingsHeader.vue'; -import AccessToken from './AccessToken.vue'; +import TokenList from './TokenList.vue'; import MfaSettingsCard from './MfaSettingsCard.vue'; import Policy from 'dashboard/components/policy.vue'; import RadioCard from 'dashboard/components-next/radioCard/RadioCard.vue'; @@ -40,9 +44,12 @@ export default { ChangePassword, NotificationPreferences, AudioNotifications, - AccessToken, + TokenList, MfaSettingsCard, BaseSettingsHeader, + OnClickOutside, + NextButton, + DropdownMenu, }, setup() { const { isEditorHotKeyEnabled, updateUISettings } = useUISettings(); @@ -92,6 +99,7 @@ export default { ], notificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS], audioNotificationPermissions: [...ROLES, ...CONVERSATION_PERMISSIONS], + isCreateTokenMenuOpen: false, }; }, computed: { @@ -103,6 +111,41 @@ export default { isMfaEnabled() { return parseBoolean(window.chatwootConfig?.isMfaEnabled); }, + accessTokens() { + return [ + { scope: 'full', value: this.currentUser.access_token }, + { scope: 'read_only', value: this.currentUser.read_only_access_token }, + ].filter(token => token.value); + }, + apiReferenceUrl() { + return getHelpUrlForFeature('access_token_api'); + }, + cliUrl() { + return getHelpUrlForFeature('chatwoot_cli'); + }, + createTokenMenuItems() { + // Each user holds at most one token per scope, so only offer scopes + // that don't have a token yet. + const presentScopes = this.accessTokens.map(token => token.scope); + return [ + { + label: this.$t( + 'PROFILE_SETTINGS.FORM.ACCESS_TOKEN.SCOPES.FULL_LABEL' + ), + icon: 'i-lucide-shield-check', + value: 'full', + }, + { + label: this.$t( + 'PROFILE_SETTINGS.FORM.ACCESS_TOKEN.SCOPES.READ_ONLY_LABEL' + ), + icon: 'i-lucide-eye', + value: 'read_only', + }, + ] + .filter(item => !presentScopes.includes(item.value)) + .map(item => ({ ...item, action: 'create' })); + }, }, mounted() { if (this.currentUserId) { @@ -192,6 +235,17 @@ export default { await copyTextToClipboard(value); useAlert(this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL')); }, + onCreateToken({ value }) { + this.isCreateTokenMenuOpen = false; + this.resetToken(value); + }, + async resetToken(scope) { + if (scope === 'read_only') { + await this.resetReadOnlyAccessToken(); + } else { + await this.resetAccessToken(); + } + }, async resetAccessToken() { const success = await this.$store.dispatch('resetAccessToken'); if (success) { @@ -353,25 +407,52 @@ export default { replaceInstallationName($t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE')) " > - - - - + + + +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/TokenList.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/TokenList.vue new file mode 100644 index 000000000..accd6eb4e --- /dev/null +++ b/app/javascript/dashboard/routes/dashboard/settings/profile/TokenList.vue @@ -0,0 +1,141 @@ + + +