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 @@
+
+
+
+
+
+
+
+
+
+ {{ tk('EMPTY.TITLE') }}
+
+
+ {{ tk('EMPTY.DESCRIPTION') }}
+
+
+
+
+
+ |
+ {{ tk('TABLE.TOKEN') }}
+ |
+
+ {{ tk('TABLE.PERMISSION') }}
+ |
+ |
+
+
+
+
+ |
+
+
+ {{
+ isRevealed(token.scope) ? token.value : maskToken(token.value)
+ }}
+
+
+
+
+ |
+
+
+
+ {{ SCOPE_META[token.scope].label() }}
+
+ |
+
+
+ |
+
+
+
+
+