diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3fd4f1a31..9e8c36fdb 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -4,5 +4,15 @@ FROM ghcr.io/chatwoot/chatwoot_codespace:latest # Do the set up required for chatwoot app WORKDIR /workspace + +# Copy dependency files first for better caching +COPY package.json pnpm-lock.yaml ./ +COPY Gemfile Gemfile.lock ./ + +# Install dependencies (will be cached if files don't change) +RUN pnpm install --frozen-lockfile && \ + gem install bundler && \ + bundle install --jobs=$(nproc) + +# Copy source code after dependencies are installed COPY . /workspace -RUN yarn && gem install bundler && bundle install diff --git a/.devcontainer/Dockerfile.base b/.devcontainer/Dockerfile.base index fe31dc42e..2b74602b8 100644 --- a/.devcontainer/Dockerfile.base +++ b/.devcontainer/Dockerfile.base @@ -1,12 +1,16 @@ - -ARG VARIANT +ARG VARIANT="ubuntu-22.04" FROM mcr.microsoft.com/vscode/devcontainers/base:0-${VARIANT} +ENV DEBIAN_FRONTEND=noninteractive + ARG NODE_VERSION ARG RUBY_VERSION ARG USER_UID ARG USER_GID +ARG PNPM_VERSION="10.2.0" +ENV PNPM_VERSION ${PNPM_VERSION} +ENV RUBY_CONFIGURE_OPTS=--disable-install-doc # Update args in docker-compose.yaml to set the UID/GID of the "vscode" user. RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ @@ -15,61 +19,80 @@ RUN if [ "$USER_GID" != "1000" ] || [ "$USER_UID" != "1000" ]; then \ && chmod -R $USER_UID:$USER_GID /home/vscode; \ fi -RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ - && apt-get -y install --no-install-recommends \ - build-essential \ - libssl-dev \ - zlib1g-dev \ - gnupg2 \ - tar \ - tzdata \ - postgresql-client \ - libpq-dev \ - yarn \ - git \ - imagemagick \ - tmux \ - zsh \ - git-flow \ - npm \ - libyaml-dev +RUN NODE_MAJOR=$(echo $NODE_VERSION | cut -d. -f1) \ + && curl -fsSL https://deb.nodesource.com/setup_${NODE_MAJOR}.x | bash - \ + && apt-get update \ + && apt-get -y install --no-install-recommends \ + build-essential \ + libssl-dev \ + zlib1g-dev \ + gnupg \ + tar \ + tzdata \ + postgresql-client \ + libpq-dev \ + git \ + imagemagick \ + libyaml-dev \ + curl \ + ca-certificates \ + tmux \ + nodejs \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -# Install rbenv and ruby -RUN git clone https://github.com/rbenv/rbenv.git ~/.rbenv \ +# Install rbenv and ruby for root user first +RUN git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv \ && echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc \ && echo 'eval "$(rbenv init -)"' >> ~/.bashrc ENV PATH "/root/.rbenv/bin/:/root/.rbenv/shims/:$PATH" -RUN git clone https://github.com/rbenv/ruby-build.git && \ +RUN git clone --depth 1 https://github.com/rbenv/ruby-build.git && \ PREFIX=/usr/local ./ruby-build/install.sh RUN rbenv install $RUBY_VERSION && \ rbenv global $RUBY_VERSION && \ rbenv versions -# Install overmind +# Set up rbenv for vscode user +RUN su - vscode -c "git clone --depth 1 https://github.com/rbenv/rbenv.git ~/.rbenv" \ + && su - vscode -c "echo 'export PATH=\"\$HOME/.rbenv/bin:\$PATH\"' >> ~/.bashrc" \ + && su - vscode -c "echo 'eval \"\$(rbenv init -)\"' >> ~/.bashrc" \ + && su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv install $RUBY_VERSION" \ + && su - vscode -c "PATH=\"/home/vscode/.rbenv/bin:\$PATH\" rbenv global $RUBY_VERSION" + +# Install overmind and gh in single layer RUN curl -L https://github.com/DarthSim/overmind/releases/download/v2.1.0/overmind-v2.1.0-linux-amd64.gz > overmind.gz \ && gunzip overmind.gz \ - && sudo mv overmind /usr/local/bin \ - && chmod +x /usr/local/bin/overmind - - -# Install gh -RUN curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ - && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ - && sudo apt update \ - && sudo apt install gh + && mv overmind /usr/local/bin \ + && chmod +x /usr/local/bin/overmind \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | tee /etc/apt/sources.list.d/github-cli.list > /dev/null \ + && apt-get update \ + && apt-get install -y --no-install-recommends gh \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* # Do the set up required for chatwoot app WORKDIR /workspace -COPY . /workspace +RUN chown vscode:vscode /workspace -# set up ruby -COPY Gemfile Gemfile.lock ./ -RUN gem install bundler && bundle install +# set up node js and pnpm in single layer +RUN npm install -g pnpm@${PNPM_VERSION} \ + && npm cache clean --force -# set up node js -RUN npm install n -g && \ - n $NODE_VERSION -RUN npm install --global yarn -RUN yarn +# Switch to vscode user +USER vscode +ENV PATH="/home/vscode/.rbenv/bin:/home/vscode/.rbenv/shims:$PATH" + +# Copy dependency files first for better caching +COPY --chown=vscode:vscode Gemfile Gemfile.lock package.json pnpm-lock.yaml ./ + +# Install dependencies as vscode user +RUN eval "$(rbenv init -)" \ + && gem install bundler -N \ + && bundle install --jobs=$(nproc) \ + && pnpm install --frozen-lockfile + +# Copy source code after dependencies are installed +COPY --chown=vscode:vscode . /workspace diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index d2dac356b..c51338b51 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -23,15 +23,15 @@ // 5432 postgres // 6379 redis // 1025,8025 mailhog - "forwardPorts": [8025, 3000, 3035], + "forwardPorts": [8025, 3000, 3036], - "postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && yarn", + "postCreateCommand": ".devcontainer/scripts/setup.sh && POSTGRES_STATEMENT_TIMEOUT=600s bundle exec rake db:chatwoot_prepare && pnpm install", "portsAttributes": { "3000": { "label": "Rails Server" }, - "3035": { - "label": "Webpack Dev Server" + "3036": { + "label": "Vite Dev Server" }, "8025": { "label": "Mailhog UI" diff --git a/.devcontainer/docker-compose.base.yml b/.devcontainer/docker-compose.base.yml new file mode 100644 index 000000000..6932b5f10 --- /dev/null +++ b/.devcontainer/docker-compose.base.yml @@ -0,0 +1,18 @@ +# Docker Compose file for building the base image in GitHub Actions +# Usage: docker-compose -f .devcontainer/docker-compose.base.yml build base + +version: '3' + +services: + base: + build: + context: .. + dockerfile: .devcontainer/Dockerfile.base + args: + VARIANT: 'ubuntu-22.04' + NODE_VERSION: '23.7.0' + RUBY_VERSION: '3.4.4' + # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. + USER_UID: '1000' + USER_GID: '1000' + image: ghcr.io/chatwoot/chatwoot_codespace:latest diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml index 21a9fe909..a9185ea09 100644 --- a/.devcontainer/docker-compose.yml +++ b/.devcontainer/docker-compose.yml @@ -5,19 +5,6 @@ version: '3' services: - base: - build: - context: .. - dockerfile: .devcontainer/Dockerfile.base - args: - VARIANT: 'ubuntu-22.04' - NODE_VERSION: '23.7.0' - RUBY_VERSION: '3.4.4' - # On Linux, you may need to update USER_UID and USER_GID below if not your local UID is not 1000. - USER_UID: '1000' - USER_GID: '1000' - image: base:latest - app: build: context: .. diff --git a/.devcontainer/scripts/setup.sh b/.devcontainer/scripts/setup.sh index 4ffee2d3a..6beb2ff57 100755 --- a/.devcontainer/scripts/setup.sh +++ b/.devcontainer/scripts/setup.sh @@ -2,12 +2,7 @@ cp .env.example .env sed -i -e '/REDIS_URL/ s/=.*/=redis:\/\/localhost:6379/' .env sed -i -e '/POSTGRES_HOST/ s/=.*/=localhost/' .env sed -i -e '/SMTP_ADDRESS/ s/=.*/=localhost/' .env -sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.githubpreview.dev/" .env -sed -i -e "/WEBPACKER_DEV_SERVER_PUBLIC/ s/=.*/=https:\/\/$CODESPACE_NAME-3035.githubpreview.dev/" .env -# uncomment the webpacker env variable -sed -i -e '/WEBPACKER_DEV_SERVER_PUBLIC/s/^# //' .env -# fix the error with webpacker -echo 'export NODE_OPTIONS=--openssl-legacy-provider' >> ~/.zshrc +sed -i -e "/FRONTEND_URL/ s/=.*/=https:\/\/$CODESPACE_NAME-3000.app.github.dev/" .env # codespaces make the ports public -gh codespace ports visibility 3000:public 3035:public 8025:public -c $CODESPACE_NAME +gh codespace ports visibility 3000:public 3036:public 8025:public -c $CODESPACE_NAME diff --git a/.github/workflows/publish_codespace_image.yml b/.github/workflows/publish_codespace_image.yml index 647608473..5da4fda05 100644 --- a/.github/workflows/publish_codespace_image.yml +++ b/.github/workflows/publish_codespace_image.yml @@ -19,6 +19,5 @@ jobs: - name: Build the Codespace Base Image run: | - docker-compose -f .devcontainer/docker-compose.yml build base - docker tag base:latest ghcr.io/chatwoot/chatwoot_codespace:latest + docker compose -f .devcontainer/docker-compose.base.yml build base docker push ghcr.io/chatwoot/chatwoot_codespace:latest 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/assets/scss/widgets/_tabs.scss b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss index 72a2e6be8..72773de7f 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_tabs.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_tabs.scss @@ -3,7 +3,7 @@ } .tabs--container--with-border { - @apply border-b border-n-weak; + @apply border-b border-b-n-weak; } .tabs--container--compact.tab--chat-type { diff --git a/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue b/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue new file mode 100644 index 000000000..ec3a8d03a --- /dev/null +++ b/app/javascript/dashboard/components-next/Conversation/SidepanelSwitch.vue @@ -0,0 +1,87 @@ + + + + + + + + diff --git a/app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue b/app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue similarity index 50% rename from app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue rename to app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue index 78a345093..13d528240 100644 --- a/app/javascript/dashboard/components-next/copilot/CopilotHeader.story.vue +++ b/app/javascript/dashboard/components-next/SidebarActionsHeader.story.vue @@ -1,21 +1,29 @@ - + - + diff --git a/app/javascript/dashboard/components-next/SidebarActionsHeader.vue b/app/javascript/dashboard/components-next/SidebarActionsHeader.vue new file mode 100644 index 000000000..210ddfa0e --- /dev/null +++ b/app/javascript/dashboard/components-next/SidebarActionsHeader.vue @@ -0,0 +1,47 @@ + + + + + + {{ title }} + + + + + + + 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 @@ + + + + + + + {{ count }} + + + + + + + {{ count }} + + + + + 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/components-next/copilot/Copilot.vue b/app/javascript/dashboard/components-next/copilot/Copilot.vue index 5feb474a6..6fb45c278 100644 --- a/app/javascript/dashboard/components-next/copilot/Copilot.vue +++ b/app/javascript/dashboard/components-next/copilot/Copilot.vue @@ -3,13 +3,15 @@ import { nextTick, ref, watch } from 'vue'; import { useI18n } from 'vue-i18n'; import { useTrack } from 'dashboard/composables'; import { COPILOT_EVENTS } from 'dashboard/helper/AnalyticsHelper/events'; +import { useUISettings } from 'dashboard/composables/useUISettings'; import CopilotInput from './CopilotInput.vue'; import CopilotLoader from './CopilotLoader.vue'; import CopilotAgentMessage from './CopilotAgentMessage.vue'; import CopilotAssistantMessage from './CopilotAssistantMessage.vue'; import ToggleCopilotAssistant from './ToggleCopilotAssistant.vue'; -import Icon from '../icon/Icon.vue'; +import Icon from 'dashboard/components-next/icon/Icon.vue'; +import SidebarActionsHeader from 'dashboard/components-next/SidebarActionsHeader.vue'; const props = defineProps({ supportAgent: { @@ -54,10 +56,6 @@ const useSuggestion = opt => { useTrack(COPILOT_EVENTS.SEND_SUGGESTED); }; -const handleReset = () => { - emit('reset'); -}; - const chatContainer = ref(null); const scrollToBottom = async () => { @@ -82,6 +80,21 @@ const promptOptions = [ }, ]; +const { updateUISettings } = useUISettings(); + +const closeCopilotPanel = () => { + updateUISettings({ + is_copilot_panel_open: false, + is_contact_sidebar_open: false, + }); +}; + +const handleSidebarAction = action => { + if (action === 'reset') { + emit('reset'); + } +}; + watch( [() => props.messages, () => props.isCaptainTyping], () => { @@ -93,6 +106,18 @@ watch( + emit('setAssistant', $event)" /> - - - {{ $t('CAPTAIN.COPILOT.RESET') }} - diff --git a/app/javascript/dashboard/components-next/copilot/CopilotHeader.vue b/app/javascript/dashboard/components-next/copilot/CopilotHeader.vue deleted file mode 100644 index c7a8696f3..000000000 --- a/app/javascript/dashboard/components-next/copilot/CopilotHeader.vue +++ /dev/null @@ -1,32 +0,0 @@ - - - - - - - {{ $t('CAPTAIN.COPILOT.TITLE') }} - - - - - - - - diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index e3a533437..0bfcf4142 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -519,7 +519,7 @@ const menuItems = computed(() => { - + { class="flex flex-col flex-shrink-0 bg-n-solid-1 conversations-list-wrap" :class="[ { hidden: !showConversationList }, - isOnExpandedLayout ? 'basis-full' : 'w-[360px] 2xl:w-[420px]', + isOnExpandedLayout ? 'basis-full' : 'w-[340px] 2xl:w-[412px]', ]" > diff --git a/app/javascript/dashboard/components/ChatListHeader.vue b/app/javascript/dashboard/components/ChatListHeader.vue index 286d6d6c5..b4ce9f342 100644 --- a/app/javascript/dashboard/components/ChatListHeader.vue +++ b/app/javascript/dashboard/components/ChatListHeader.vue @@ -80,16 +80,14 @@ const toggleConversationLayout = () => { {{ pageTitle }} diff --git a/app/javascript/dashboard/components/Snackbar.vue b/app/javascript/dashboard/components/Snackbar.vue index 3a23a15ef..0404c4ba8 100644 --- a/app/javascript/dashboard/components/Snackbar.vue +++ b/app/javascript/dashboard/components/Snackbar.vue @@ -20,7 +20,7 @@ export default { {{ message }} @@ -29,7 +29,7 @@ export default { {{ action.message }} diff --git a/app/javascript/dashboard/components/SnackbarContainer.vue b/app/javascript/dashboard/components/SnackbarContainer.vue index b55d3865c..8c0daebaf 100644 --- a/app/javascript/dashboard/components/SnackbarContainer.vue +++ b/app/javascript/dashboard/components/SnackbarContainer.vue @@ -1,62 +1,72 @@ - - - - + + + + diff --git a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue index 01fb46187..26fa7466a 100644 --- a/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue +++ b/app/javascript/dashboard/components/widgets/ChatTypeTabs.vue @@ -48,12 +48,13 @@ useKeyboardEvents(keyboardEvents); - + import { mapGetters } from 'vuex'; -import { useKeyboardEvents } from 'dashboard/composables/useKeyboardEvents'; import BackButton from '../BackButton.vue'; import inboxMixin from 'shared/mixins/inboxMixin'; import InboxName from '../InboxName.vue'; @@ -29,11 +28,7 @@ export default { props: { chat: { type: Object, - default: () => {}, - }, - isContactPanelOpen: { - type: Boolean, - default: false, + default: () => ({}), }, showBackButton: { type: Boolean, @@ -44,15 +39,6 @@ export default { default: false, }, }, - emits: ['contactPanelToggle'], - setup(props, { emit }) { - const keyboardEvents = { - 'Alt+KeyO': { - action: () => emit('contactPanelToggle'), - }, - }; - useKeyboardEvents(keyboardEvents); - }, computed: { ...mapGetters({ currentChat: 'getSelectedChat', @@ -99,13 +85,6 @@ export default { } return this.$t('CONVERSATION.HEADER.SNOOZED_UNTIL_NEXT_REPLY'); }, - contactPanelToggleText() { - return `${ - this.isContactPanelOpen - ? this.$t('CONVERSATION.HEADER.CLOSE') - : this.$t('CONVERSATION.HEADER.OPEN') - } ${this.$t('CONVERSATION.HEADER.DETAILS')}`; - }, inbox() { const { inbox_id: inboxId } = this.chat; return this.$store.getters['inboxes/getInbox'](inboxId); @@ -133,7 +112,7 @@ export default { - + {{ currentContact.name }} @@ -180,19 +160,11 @@ export default { {{ snoozedDisplayText }} - -import { computed, ref } from 'vue'; +import { computed } from 'vue'; import CopilotContainer from '../../copilot/CopilotContainer.vue'; import ContactPanel from 'dashboard/routes/dashboard/conversation/ContactPanel.vue'; -import TabBar from 'dashboard/components-next/tabbar/TabBar.vue'; -import { useI18n } from 'vue-i18n'; import { useMapGetter } from 'dashboard/composables/store'; import { FEATURE_FLAGS } from '../../../featureFlags'; +import { useUISettings } from 'dashboard/composables/useUISettings'; const props = defineProps({ currentChat: { @@ -14,33 +13,8 @@ const props = defineProps({ }, }); -const emit = defineEmits(['toggleContactPanel']); - -const { t } = useI18n(); - const channelType = computed(() => props.currentChat?.meta?.channel || ''); -const CONTACT_TABS_OPTIONS = [ - { key: 'CONTACT', value: 'contact' }, - { key: 'COPILOT', value: 'copilot' }, -]; - -const tabs = computed(() => { - return CONTACT_TABS_OPTIONS.map(tab => ({ - label: t(`CONVERSATION.SIDEBAR.${tab.key}`), - value: tab.value, - })); -}); -const activeTab = ref(0); -const toggleContactPanel = () => { - emit('toggleContactPanel'); -}; - -const handleTabChange = selectedTab => { - activeTab.value = tabs.value.findIndex( - tabItem => tabItem.value === selectedTab.value - ); -}; const currentAccountId = useMapGetter('getCurrentAccountId'); const isFeatureEnabledonAccount = useMapGetter( 'accounts/isFeatureEnabledonAccount' @@ -49,29 +23,37 @@ const isFeatureEnabledonAccount = useMapGetter( const showCopilotTab = computed(() => isFeatureEnabledonAccount.value(currentAccountId.value, FEATURE_FLAGS.CAPTAIN) ); + +const { uiSettings } = useUISettings(); + +const activeTab = computed(() => { + const { + is_contact_sidebar_open: isContactSidebarOpen, + is_copilot_panel_open: isCopilotPanelOpen, + } = uiSettings.value; + + if (isContactSidebarOpen) { + return 0; + } + if (isCopilotPanelOpen) { + return 1; + } + return null; +}); - - - - - - - getUserPermissions(currentUser.value, currentAccountId.value) -); +const { shouldShow, isFeatureFlagEnabled } = usePolicy(); const TABS_CONFIG = { all: { @@ -111,47 +105,67 @@ const TABS_CONFIG = { }, articles: { permissions: [...ROLES, PORTAL_PERMISSIONS], + featureFlag: FEATURE_FLAGS.HELP_CENTER, count: () => mappedArticles.value.length, }, }; const tabs = computed(() => { - const configs = Object.entries(TABS_CONFIG).map(([key, config]) => ({ - key, - name: t(`SEARCH.TABS.${key.toUpperCase()}`), - count: config.count(), - showBadge: key !== 'all', - permissions: config.permissions, - })); - - return filterItemsByPermission( - configs, - userPermissions.value, - item => item.permissions - ); + return Object.entries(TABS_CONFIG) + .map(([key, config]) => ({ + key, + name: t(`SEARCH.TABS.${key.toUpperCase()}`), + count: config.count(), + showBadge: key !== 'all', + permissions: config.permissions, + featureFlag: config.featureFlag, + })) + .filter(config => { + // why the double check, glad you asked. + // Some features are marked as premium features, that means + // the feature will be visible, but a Paywall will be shown instead + // this works for pages and routes, but fails for UI elements like search here + // so we explicitly check if the feature is enabled + return ( + shouldShow(config.featureFlag, config.permissions, null) && + isFeatureFlagEnabled(config.featureFlag) + ); + }); }); const totalSearchResultsCount = computed(() => { - const permissionCounts = { - contacts: { + const permissionCounts = [ + { permissions: [...ROLES, CONTACT_PERMISSIONS], count: () => contacts.value.length, }, - conversations: { + { permissions: [...ROLES, ...CONVERSATION_PERMISSIONS], count: () => conversations.value.length + messages.value.length, }, - articles: { + { permissions: [...ROLES, PORTAL_PERMISSIONS], + featureFlag: FEATURE_FLAGS.HELP_CENTER, count: () => articles.value.length, }, - }; - return filterItemsByPermission( - permissionCounts, - userPermissions.value, - item => item.permissions, - (_, item) => item.count - ).reduce((total, count) => total + count(), 0); + ]; + + return permissionCounts + .filter(config => { + // why the double check, glad you asked. + // Some features are marked as premium features, that means + // the feature will be visible, but a Paywall will be shown instead + // this works for pages and routes, but fails for UI elements like search here + // so we explicitly check if the feature is enabled + return ( + shouldShow(config.featureFlag, config.permissions, null) && + isFeatureFlagEnabled(config.featureFlag) + ); + }) + .map(config => { + return config.count(); + }) + .reduce((sum, count) => sum + count, 0); }); const activeTabIndex = computed(() => { @@ -355,7 +369,9 @@ onUnmounted(() => { {}, - }, }); const { @@ -89,8 +85,6 @@ watch(conversationId, (newConversationId, prevConversationId) => { watch(contactId, getContactDetails); -const onPanelToggle = props.onToggle; - const onDragEnd = () => { dragging.value = false; updateUISettings({ @@ -98,6 +92,13 @@ const onDragEnd = () => { }); }; +const closeContactPanel = () => { + updateUISettings({ + is_contact_sidebar_open: false, + is_copilot_panel_open: false, + }); +}; + onMounted(() => { conversationSidebarItems.value = conversationSidebarItemsOrder.value; getContactDetails(); @@ -107,11 +108,11 @@ onMounted(() => { - + + > + + + 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 = () => { > @@ -46,15 +53,28 @@ 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/javascript/portal/portalHelpers.js b/app/javascript/portal/portalHelpers.js index bcfafa03f..6b4a96a0e 100644 --- a/app/javascript/portal/portalHelpers.js +++ b/app/javascript/portal/portalHelpers.js @@ -2,6 +2,7 @@ import { createApp } from 'vue'; import VueDOMPurifyHTML from 'vue-dompurify-html'; import { domPurifyConfig } from '../shared/helpers/HTMLSanitizer'; import { directive as onClickaway } from 'vue3-click-away'; +import { isSameHost } from '@chatwoot/utils'; import slugifyWithCounter from '@sindresorhus/slugify'; import PublicArticleSearch from './components/PublicArticleSearch.vue'; @@ -25,52 +26,6 @@ export const getHeadingsfromTheArticle = () => { return rows; }; -/** - * Converts various input formats to URL objects. - * Handles URL objects, domain strings, relative paths, and full URLs. - * @param {string|URL} input - Input to convert to URL object - * @returns {URL|null} URL object or null if input is invalid - */ -const toURL = input => { - if (!input) return null; - if (input instanceof URL) return input; - - if ( - typeof input === 'string' && - !input.includes('://') && - !input.startsWith('/') - ) { - return new URL(`https://${input}`); - } - - if (typeof input === 'string' && input.startsWith('/')) { - return new URL(input, window.location.origin); - } - - return new URL(input); -}; - -/** - * Determines if two URLs belong to the same host by comparing their normalized URL objects. - * Handles various input formats including URL objects, domain strings, relative paths, and full URLs. - * Returns false if either URL cannot be parsed or normalized. - * @param {string|URL} url1 - First URL to compare - * @param {string|URL} url2 - Second URL to compare - * @returns {boolean} True if both URLs have the same host, false otherwise - */ -const isSameHost = (url1, url2) => { - try { - const urlObj1 = toURL(url1); - const urlObj2 = toURL(url2); - - if (!urlObj1 || !urlObj2) return false; - - return urlObj1.hostname === urlObj2.hostname; - } catch (error) { - return false; - } -}; - export const openExternalLinksInNewTab = () => { const { customDomain, hostURL } = window.portalConfig; const isOnArticlePage = 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/environments/development.rb b/config/environments/development.rb index 557000065..7f72e6d2f 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -62,6 +62,15 @@ Rails.application.configure do # Disable host check during development config.hosts = nil + + # GitHub Codespaces configuration + if ENV['CODESPACES'] + # Allow web console access from any IP + config.web_console.whitelisted_ips = %w(0.0.0.0/0 ::/0) + # Allow CSRF from codespace URLs + config.force_ssl = false + config.action_controller.forgery_protection_origin_check = false + end # customize using the environment variables config.log_level = ENV.fetch('LOG_LEVEL', 'debug').to_sym diff --git a/config/features.yml b/config/features.yml index eacbd0a72..131456c72 100644 --- a/config/features.yml +++ b/config/features.yml @@ -146,7 +146,7 @@ premium: true - name: chatwoot_v4 display_name: Chatwoot V4 - enabled: false + enabled: true - name: report_v4 display_name: Report V4 enabled: true diff --git a/config/routes.rb b/config/routes.rb index c4df0d801..7712dbff2 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/package.json b/package.json index 406a640a9..d18c48e3e 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", "@chatwoot/prosemirror-schema": "1.1.1-next", - "@chatwoot/utils": "^0.0.43", + "@chatwoot/utils": "^0.0.45", "@formkit/core": "^1.6.7", "@formkit/vue": "^1.6.7", "@hcaptcha/vue3-hcaptcha": "^1.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 091d7364d..ca735a7e6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: 1.1.1-next version: 1.1.1-next '@chatwoot/utils': - specifier: ^0.0.43 - version: 0.0.43 + specifier: ^0.0.45 + version: 0.0.45 '@formkit/core': specifier: ^1.6.7 version: 1.6.7 @@ -406,8 +406,8 @@ packages: '@chatwoot/prosemirror-schema@1.1.1-next': resolution: {integrity: sha512-/M2qZ+ZF7GlQNt1riwVP499fvp3hxSqd5iy8hxyF9pkj9qQ+OKYn5JK+v3qwwqQY3IxhmNOn1Lp6tm7vstrd9Q==} - '@chatwoot/utils@0.0.43': - resolution: {integrity: sha512-kMIXAGebCak9qOi68QnGer+rQLLo/z2N9cR+7tvGdZCW0ThDiVCF7JbHYHVDlYsdDFIx0FLlyIdCfEbooVT2Dw==} + '@chatwoot/utils@0.0.45': + resolution: {integrity: sha512-zqmuri6MrEFAY1tLv7Z3HBy4Ig60LhSrLkEiHegVsOVSxPv4Bedq+xmAW7LphvcLNgbkkvu17MU91gvMVlpEHw==} engines: {node: '>=10'} '@codemirror/commands@6.7.0': @@ -5262,7 +5262,7 @@ snapshots: prosemirror-utils: 1.2.2(prosemirror-model@1.22.3)(prosemirror-state@1.4.3) prosemirror-view: 1.34.1 - '@chatwoot/utils@0.0.43': + '@chatwoot/utils@0.0.45': dependencies: date-fns: 2.30.0 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 diff --git a/swagger/definitions/index.yml b/swagger/definitions/index.yml index 57e10e2dd..faf947217 100644 --- a/swagger/definitions/index.yml +++ b/swagger/definitions/index.yml @@ -60,6 +60,10 @@ webhook: $ref: ./resource/webhook.yml account: $ref: ./resource/account.yml +account_detail: + $ref: ./resource/account_detail.yml +account_show_response: + $ref: ./resource/account_show_response.yml account_user: $ref: ./resource/account_user.yml platform_account: @@ -87,6 +91,9 @@ public_inbox: account_create_update_payload: $ref: ./request/account/create_update_payload.yml +account_update_payload: + $ref: ./request/account/update_payload.yml + account_user_create_update_payload: $ref: ./request/account_user/create_update_payload.yml diff --git a/swagger/definitions/request/account/update_payload.yml b/swagger/definitions/request/account/update_payload.yml new file mode 100644 index 000000000..b2e47cbc2 --- /dev/null +++ b/swagger/definitions/request/account/update_payload.yml @@ -0,0 +1,49 @@ +type: object +properties: + name: + type: string + description: Name of the account + example: 'My Account' + locale: + type: string + description: The locale of the account + example: 'en' + domain: + type: string + description: The domain of the account + example: 'example.com' + support_email: + type: string + description: The support email of the account + example: 'support@example.com' + # Settings parameters (stored in settings JSONB column) + auto_resolve_after: + type: integer + minimum: 10 + maximum: 1439856 + nullable: true + description: Auto resolve conversations after specified minutes + example: 1440 + auto_resolve_message: + type: string + nullable: true + description: Message to send when auto resolving + example: "This conversation has been automatically resolved due to inactivity" + auto_resolve_ignore_waiting: + type: boolean + nullable: true + description: Whether to ignore waiting conversations for auto resolve + example: false + # Custom attributes parameters (stored in custom_attributes JSONB column) + industry: + type: string + description: Industry type + example: "Technology" + company_size: + type: string + description: Company size + example: "50-100" + timezone: + type: string + description: Account timezone + example: "UTC" \ No newline at end of file diff --git a/swagger/definitions/resource/account_detail.yml b/swagger/definitions/resource/account_detail.yml new file mode 100644 index 000000000..0ff463bae --- /dev/null +++ b/swagger/definitions/resource/account_detail.yml @@ -0,0 +1,84 @@ +type: object +properties: + id: + type: number + description: Account ID + name: + type: string + description: Name of the account + locale: + type: string + description: The locale of the account + domain: + type: string + description: The domain of the account + support_email: + type: string + description: The support email of the account + status: + type: string + description: The status of the account + created_at: + type: string + format: date-time + description: The creation date of the account + cache_keys: + type: object + description: Cache keys for the account + features: + type: array + items: + type: string + description: Enabled features for the account + settings: + type: object + description: Account settings + properties: + auto_resolve_after: + type: number + description: Auto resolve conversations after specified minutes + auto_resolve_message: + type: string + description: Message to send when auto resolving + auto_resolve_ignore_waiting: + type: boolean + description: Whether to ignore waiting conversations for auto resolve + custom_attributes: + type: object + description: Custom attributes of the account + properties: + plan_name: + type: string + description: Subscription plan name + subscribed_quantity: + type: number + description: Subscribed quantity + subscription_status: + type: string + description: Subscription status + subscription_ends_on: + type: string + format: date + description: Subscription end date + industry: + type: string + description: Industry type + company_size: + type: string + description: Company size + timezone: + type: string + description: Account timezone + logo: + type: string + description: Account logo URL + onboarding_step: + type: string + description: Current onboarding step + marked_for_deletion_at: + type: string + format: date-time + description: When account was marked for deletion + marked_for_deletion_reason: + type: string + description: Reason for account deletion \ No newline at end of file diff --git a/swagger/definitions/resource/account_show_response.yml b/swagger/definitions/resource/account_show_response.yml new file mode 100644 index 000000000..208b27218 --- /dev/null +++ b/swagger/definitions/resource/account_show_response.yml @@ -0,0 +1,13 @@ +allOf: + - $ref: '#/components/schemas/account_detail' + - type: object + properties: + latest_chatwoot_version: + type: string + description: Latest version of Chatwoot available + example: "3.0.0" + subscribed_features: + type: array + items: + type: string + description: List of subscribed enterprise features (if enterprise edition is enabled) \ No newline at end of file diff --git a/swagger/paths/application/accounts/show.yml b/swagger/paths/application/accounts/show.yml new file mode 100644 index 000000000..56e0c8052 --- /dev/null +++ b/swagger/paths/application/accounts/show.yml @@ -0,0 +1,28 @@ +tags: + - Account +operationId: get-account-details +summary: Get account details +description: Get the details of the current account +security: + - userApiKey: [] +parameters: + - $ref: '#/components/parameters/account_id' +responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/account_show_response' + '401': + description: Unauthorized + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' \ No newline at end of file diff --git a/swagger/paths/application/accounts/update.yml b/swagger/paths/application/accounts/update.yml new file mode 100644 index 000000000..23c9c40f8 --- /dev/null +++ b/swagger/paths/application/accounts/update.yml @@ -0,0 +1,43 @@ +tags: + - Account +operationId: update-account +summary: Update account +description: Update account details, settings, and custom attributes +security: + - userApiKey: [] +parameters: + - $ref: '#/components/parameters/account_id' +requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/account_update_payload' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/account_update_payload' +responses: + '200': + description: Success + content: + application/json: + schema: + $ref: '#/components/schemas/account_detail' + '401': + description: Unauthorized (requires administrator role) + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' + '404': + description: Account not found + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' + '422': + description: Validation error + content: + application/json: + schema: + $ref: '#/components/schemas/bad_request_error' \ No newline at end of file diff --git a/swagger/paths/index.yml b/swagger/paths/index.yml index 379b2eef0..a70800b89 100644 --- a/swagger/paths/index.yml +++ b/swagger/paths/index.yml @@ -166,6 +166,15 @@ # ------------ Application API routes ------------# +# Accounts +/api/v1/accounts/{id}: + parameters: + - $ref: '#/components/parameters/account_id' + get: + $ref: ./application/accounts/show.yml + patch: + $ref: ./application/accounts/update.yml + # AgentBots /api/v1/accounts/{account_id}/agent_bots: parameters: diff --git a/swagger/swagger.json b/swagger/swagger.json index 943b7a3e8..b9ecbcf27 100644 --- a/swagger/swagger.json +++ b/swagger/swagger.json @@ -1476,6 +1476,138 @@ } } }, + "/api/v1/accounts/{id}": { + "parameters": [ + { + "$ref": "#/components/parameters/account_id" + } + ], + "get": { + "tags": [ + "Account" + ], + "operationId": "get-account-details", + "summary": "Get account details", + "description": "Get the details of the current account", + "security": [ + { + "userApiKey": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/account_id" + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/account_show_response" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + }, + "404": { + "description": "Account not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + } + } + }, + "patch": { + "tags": [ + "Account" + ], + "operationId": "update-account", + "summary": "Update account", + "description": "Update account details, settings, and custom attributes", + "security": [ + { + "userApiKey": [] + } + ], + "parameters": [ + { + "$ref": "#/components/parameters/account_id" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/account_update_payload" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/account_update_payload" + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/account_detail" + } + } + } + }, + "401": { + "description": "Unauthorized (requires administrator role)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + }, + "404": { + "description": "Account not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + }, + "422": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/bad_request_error" + } + } + } + } + } + } + }, "/api/v1/accounts/{account_id}/agent_bots": { "parameters": [ { @@ -8774,6 +8906,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -9113,6 +9384,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/application_swagger.json b/swagger/tag_groups/application_swagger.json index 33e75183f..2a8162358 100644 --- a/swagger/tag_groups/application_swagger.json +++ b/swagger/tag_groups/application_swagger.json @@ -7135,6 +7135,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -7474,6 +7613,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/client_swagger.json b/swagger/tag_groups/client_swagger.json index 6d471da43..cefc39324 100644 --- a/swagger/tag_groups/client_swagger.json +++ b/swagger/tag_groups/client_swagger.json @@ -1978,6 +1978,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -2317,6 +2456,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/other_swagger.json b/swagger/tag_groups/other_swagger.json index fc61e8721..2cc045747 100644 --- a/swagger/tag_groups/other_swagger.json +++ b/swagger/tag_groups/other_swagger.json @@ -1393,6 +1393,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -1732,6 +1871,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [ diff --git a/swagger/tag_groups/platform_swagger.json b/swagger/tag_groups/platform_swagger.json index 18ec796a0..085e969ab 100644 --- a/swagger/tag_groups/platform_swagger.json +++ b/swagger/tag_groups/platform_swagger.json @@ -2154,6 +2154,145 @@ } } }, + "account_detail": { + "type": "object", + "properties": { + "id": { + "type": "number", + "description": "Account ID" + }, + "name": { + "type": "string", + "description": "Name of the account" + }, + "locale": { + "type": "string", + "description": "The locale of the account" + }, + "domain": { + "type": "string", + "description": "The domain of the account" + }, + "support_email": { + "type": "string", + "description": "The support email of the account" + }, + "status": { + "type": "string", + "description": "The status of the account" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The creation date of the account" + }, + "cache_keys": { + "type": "object", + "description": "Cache keys for the account" + }, + "features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Enabled features for the account" + }, + "settings": { + "type": "object", + "description": "Account settings", + "properties": { + "auto_resolve_after": { + "type": "number", + "description": "Auto resolve conversations after specified minutes" + }, + "auto_resolve_message": { + "type": "string", + "description": "Message to send when auto resolving" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "description": "Whether to ignore waiting conversations for auto resolve" + } + } + }, + "custom_attributes": { + "type": "object", + "description": "Custom attributes of the account", + "properties": { + "plan_name": { + "type": "string", + "description": "Subscription plan name" + }, + "subscribed_quantity": { + "type": "number", + "description": "Subscribed quantity" + }, + "subscription_status": { + "type": "string", + "description": "Subscription status" + }, + "subscription_ends_on": { + "type": "string", + "format": "date", + "description": "Subscription end date" + }, + "industry": { + "type": "string", + "description": "Industry type" + }, + "company_size": { + "type": "string", + "description": "Company size" + }, + "timezone": { + "type": "string", + "description": "Account timezone" + }, + "logo": { + "type": "string", + "description": "Account logo URL" + }, + "onboarding_step": { + "type": "string", + "description": "Current onboarding step" + }, + "marked_for_deletion_at": { + "type": "string", + "format": "date-time", + "description": "When account was marked for deletion" + }, + "marked_for_deletion_reason": { + "type": "string", + "description": "Reason for account deletion" + } + } + } + } + }, + "account_show_response": { + "allOf": [ + { + "$ref": "#/components/schemas/account_detail" + }, + { + "type": "object", + "properties": { + "latest_chatwoot_version": { + "type": "string", + "description": "Latest version of Chatwoot available", + "example": "3.0.0" + }, + "subscribed_features": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of subscribed enterprise features (if enterprise edition is enabled)" + } + } + } + ] + }, "account_user": { "type": "array", "description": "Array of account users", @@ -2493,6 +2632,66 @@ } } }, + "account_update_payload": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Name of the account", + "example": "My Account" + }, + "locale": { + "type": "string", + "description": "The locale of the account", + "example": "en" + }, + "domain": { + "type": "string", + "description": "The domain of the account", + "example": "example.com" + }, + "support_email": { + "type": "string", + "description": "The support email of the account", + "example": "support@example.com" + }, + "auto_resolve_after": { + "type": "integer", + "minimum": 10, + "maximum": 1439856, + "nullable": true, + "description": "Auto resolve conversations after specified minutes", + "example": 1440 + }, + "auto_resolve_message": { + "type": "string", + "nullable": true, + "description": "Message to send when auto resolving", + "example": "This conversation has been automatically resolved due to inactivity" + }, + "auto_resolve_ignore_waiting": { + "type": "boolean", + "nullable": true, + "description": "Whether to ignore waiting conversations for auto resolve", + "example": false + }, + "industry": { + "type": "string", + "description": "Industry type", + "example": "Technology" + }, + "company_size": { + "type": "string", + "description": "Company size", + "example": "50-100" + }, + "timezone": { + "type": "string", + "description": "Account timezone", + "example": "UTC" + } + } + }, "account_user_create_update_payload": { "type": "object", "required": [
{{ count }}