From 14ba73fc63f0d1f4cd68d246e32014776069b124 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 1 Jul 2025 13:31:02 +0530 Subject: [PATCH] fix: Revoke Linear OAuth token when integration is deleted (#11838) When users delete the Linear integration from their Chatwoot dashboard, the access token remains valid in Linear's system. This causes the integration to still appear as connected in Linear's UI, even though it's been removed from Chatwoot. Users need to manually disconnect from Linear's side to fully remove the integration. https://www.loom.com/share/5c102cbdf02e49bcb7a6fa6d409b531a?sid=0c664250-c867-4fc8-b44d-e1c1165337a7 --- .../v1/accounts/integrations/linear_controller.rb | 12 ++++++++++++ lib/linear.rb | 9 +++++++++ .../accounts/integrations/linear_controller_spec.rb | 6 ++++++ 3 files changed, 27 insertions(+) diff --git a/app/controllers/api/v1/accounts/integrations/linear_controller.rb b/app/controllers/api/v1/accounts/integrations/linear_controller.rb index bfdfff058..c121ec9a4 100644 --- a/app/controllers/api/v1/accounts/integrations/linear_controller.rb +++ b/app/controllers/api/v1/accounts/integrations/linear_controller.rb @@ -3,6 +3,7 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas before_action :fetch_hook, only: [:destroy] def destroy + revoke_linear_token @hook.destroy! head :ok end @@ -120,4 +121,15 @@ class Api::V1::Accounts::Integrations::LinearController < Api::V1::Accounts::Bas def fetch_hook @hook = Integrations::Hook.where(account: Current.account).find_by(app_id: 'linear') end + + def revoke_linear_token + return unless @hook&.access_token + + begin + linear_client = Linear.new(@hook.access_token) + linear_client.revoke_token + rescue StandardError => e + Rails.logger.error "Failed to revoke Linear token: #{e.message}" + end + end end diff --git a/lib/linear.rb b/lib/linear.rb index 8bf967fc3..d96be1b5d 100644 --- a/lib/linear.rb +++ b/lib/linear.rb @@ -1,5 +1,6 @@ class Linear BASE_URL = 'https://api.linear.app/graphql'.freeze + REVOKE_URL = 'https://api.linear.app/oauth/revoke'.freeze PRIORITY_LEVELS = (0..4).to_a def initialize(access_token) @@ -86,6 +87,14 @@ class Linear process_response(response) end + def revoke_token + response = HTTParty.post( + REVOKE_URL, + headers: { 'Authorization' => "Bearer #{@access_token}", 'Content-Type' => 'application/json' } + ) + response.success? + end + private def validate_team_and_title(params) diff --git a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb index 851c5dbaf..995b7c879 100644 --- a/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/integrations/linear_controller_spec.rb @@ -14,6 +14,12 @@ RSpec.describe 'Linear Integration API', type: :request do describe 'DELETE /api/v1/accounts/:account_id/integrations/linear' do it 'deletes the linear integration' do + # Stub the HTTP call to Linear's revoke endpoint + allow(HTTParty).to receive(:post).with( + 'https://api.linear.app/oauth/revoke', + anything + ).and_return(instance_double(HTTParty::Response, success?: true)) + delete "/api/v1/accounts/#{account.id}/integrations/linear", headers: agent.create_new_auth_token, as: :json