Merge branch 'develop' into fix/premium-feature-and-agent-guards

This commit is contained in:
Tanmay Deep Sharma
2026-07-22 18:00:41 +05:30
committed by GitHub
8 changed files with 81 additions and 24 deletions
@@ -0,0 +1,9 @@
class Api::V1::Accounts::Integrations::BaseController < Api::V1::Accounts::BaseController
private
# Managing an integration hook (create/update/destroy) is admin-only, enforced via HookPolicy.
# Subclasses opt in per action with `before_action :check_authorization, only: [...]`.
def check_authorization
authorize(:hook)
end
end
@@ -1,4 +1,4 @@
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::BaseController
class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Integrations::BaseController
before_action :fetch_hook, except: [:create]
before_action :check_authorization
@@ -35,10 +35,6 @@ class Api::V1::Accounts::Integrations::HooksController < Api::V1::Accounts::Base
@hook = Current.account.hooks.find(params[:id])
end
def check_authorization
authorize(:hook)
end
def permitted_params
params.require(:hook).permit(:app_id, :inbox_id, :status, settings: {})
end
@@ -1,6 +1,7 @@
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::BaseController
class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Integrations::BaseController
before_action :fetch_conversation, only: [:create_issue, :link_issue, :unlink_issue, :linked_issues]
before_action :fetch_hook, only: [:destroy]
before_action :check_authorization, only: [:destroy]
def destroy
revoke_linear_token
@@ -1,5 +1,6 @@
class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::BaseController
class Api::V1::Accounts::Integrations::NotionController < Api::V1::Accounts::Integrations::BaseController
before_action :fetch_hook, only: [:destroy]
before_action :check_authorization, only: [:destroy]
def destroy
@hook.destroy!
@@ -1,7 +1,8 @@
class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::BaseController
class Api::V1::Accounts::Integrations::ShopifyController < Api::V1::Accounts::Integrations::BaseController
include Shopify::IntegrationHelper
before_action :setup_shopify_context, only: [:orders]
before_action :fetch_hook, except: [:auth]
before_action :check_authorization, only: [:destroy]
before_action :validate_contact, only: [:orders]
def auth
+37 -12
View File
@@ -11,6 +11,8 @@ import { IFrameHelper } from '../helpers/utils';
import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents';
import { emitter } from 'shared/helpers/mitt';
const TRANSCRIPT_COOLDOWN_MS = 15000;
export default {
components: {
ChatInputWrap,
@@ -24,6 +26,9 @@ export default {
data() {
return {
inReplyTo: null,
isSendingTranscript: false,
transcriptCooldown: false,
transcriptCooldownTimer: null,
};
},
computed: {
@@ -57,6 +62,9 @@ export default {
mounted() {
emitter.on(BUS_EVENTS.TOGGLE_REPLY_TO_MESSAGE, this.toggleReplyTo);
},
beforeUnmount() {
clearTimeout(this.transcriptCooldownTimer);
},
methods: {
...mapActions('conversation', ['sendMessage', 'sendAttachment']),
...mapActions('conversationAttributes', ['getAttributes']),
@@ -90,19 +98,35 @@ export default {
toggleReplyTo(message) {
this.inReplyTo = message;
},
startTranscriptCooldown() {
this.transcriptCooldown = true;
clearTimeout(this.transcriptCooldownTimer);
this.transcriptCooldownTimer = setTimeout(() => {
this.transcriptCooldown = false;
}, TRANSCRIPT_COOLDOWN_MS);
},
async sendTranscript() {
if (this.hasEmail) {
try {
await sendEmailTranscript();
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'),
type: 'success',
});
} catch (error) {
emitter.$emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'),
});
}
if (
!this.hasEmail ||
this.isSendingTranscript ||
this.transcriptCooldown
) {
return;
}
this.isSendingTranscript = true;
try {
await sendEmailTranscript();
this.startTranscriptCooldown();
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_SUCCESS'),
type: 'success',
});
} catch (error) {
emitter.emit(BUS_EVENTS.SHOW_ALERT, {
message: this.$t('EMAIL_TRANSCRIPT.SEND_EMAIL_ERROR'),
});
} finally {
this.isSendingTranscript = false;
}
},
},
@@ -144,6 +168,7 @@ export default {
v-if="showEmailTranscriptButton"
type="clear"
class="font-normal"
:disabled="isSendingTranscript || transcriptCooldown"
@click="sendTranscript"
>
{{ $t('EMAIL_TRANSCRIPT.BUTTON_TEXT') }}
@@ -13,7 +13,9 @@ RSpec.describe 'Linear Integration API', type: :request do
end
describe 'DELETE /api/v1/accounts/:account_id/integrations/linear' do
it 'deletes the linear integration' do
let(:admin) { create(:user, account: account, role: :administrator) }
it 'deletes the linear integration when the user is an administrator' do
# Stub the HTTP call to Linear's revoke endpoint
allow(HTTParty).to receive(:post).with(
'https://api.linear.app/oauth/revoke',
@@ -21,11 +23,19 @@ RSpec.describe 'Linear Integration API', type: :request do
).and_return(instance_double(HTTParty::Response, success?: true))
delete "/api/v1/accounts/#{account.id}/integrations/linear",
headers: agent.create_new_auth_token,
headers: admin.create_new_auth_token,
as: :json
expect(response).to have_http_status(:ok)
expect(account.hooks.count).to eq(0)
end
it 'returns unauthorized for an agent and keeps the integration' do
delete "/api/v1/accounts/#{account.id}/integrations/linear",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:unauthorized)
expect(account.hooks.count).to eq(1)
end
end
describe 'GET /api/v1/accounts/:account_id/integrations/linear/teams' do
@@ -159,15 +159,17 @@ RSpec.describe 'Shopify Integration API', type: :request do
end
describe 'DELETE /api/v1/accounts/:account_id/integrations/shopify' do
let(:admin) { create(:user, account: account, role: :administrator) }
before do
create(:integrations_hook, :shopify, account: account)
end
context 'when it is an authenticated user' do
context 'when it is an administrator' do
it 'deletes the shopify integration' do
expect do
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
headers: agent.create_new_auth_token,
headers: admin.create_new_auth_token,
as: :json
end.to change { account.hooks.count }.by(-1)
@@ -175,6 +177,18 @@ RSpec.describe 'Shopify Integration API', type: :request do
end
end
context 'when it is an agent' do
it 'returns unauthorized and keeps the integration' do
expect do
delete "/api/v1/accounts/#{account.id}/integrations/shopify",
headers: agent.create_new_auth_token,
as: :json
end.not_to(change { account.hooks.count })
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
delete "/api/v1/accounts/#{account.id}/integrations/shopify",