diff --git a/app/controllers/api/v1/accounts/callbacks_controller.rb b/app/controllers/api/v1/accounts/callbacks_controller.rb index 37116d8d0..ff2478447 100644 --- a/app/controllers/api/v1/accounts/callbacks_controller.rb +++ b/app/controllers/api/v1/accounts/callbacks_controller.rb @@ -30,7 +30,7 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController if (page_detail = (page_details || []).detect { |page| fb_page_id == page['id'] }) update_fb_page(fb_page_id, page_detail['access_token']) - return head :ok + render and return end end @@ -44,9 +44,9 @@ class Api::V1::Accounts::CallbacksController < Api::V1::Accounts::BaseController end def update_fb_page(fb_page_id, access_token) - get_fb_page(fb_page_id)&.update!( - user_access_token: @user_access_token, page_access_token: access_token - ) + fb_page = get_fb_page(fb_page_id) + fb_page&.update!(user_access_token: @user_access_token, page_access_token: access_token) + fb_page&.reauthorized! end def get_fb_page(fb_page_id) diff --git a/app/javascript/dashboard/api/channel/fbChannel.js b/app/javascript/dashboard/api/channel/fbChannel.js index 2e86cb927..f9c937122 100644 --- a/app/javascript/dashboard/api/channel/fbChannel.js +++ b/app/javascript/dashboard/api/channel/fbChannel.js @@ -12,6 +12,13 @@ class FBChannel extends ApiClient { params ); } + + reauthorizeFacebookPage({ omniauthToken, inboxId }) { + return axios.post(`${this.baseUrl()}/callbacks/reauthorize_page`, { + omniauth_token: omniauthToken, + inbox_id: inboxId, + }); + } } export default new FBChannel(); diff --git a/app/javascript/dashboard/assets/scss/widgets/_forms.scss b/app/javascript/dashboard/assets/scss/widgets/_forms.scss index 4606ba673..d824f9d48 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_forms.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_forms.scss @@ -28,3 +28,7 @@ input { font-size: $font-size-small; font-weight: $font-weight-medium; } + +.help-text { + font-weight: $font-weight-normal; +} diff --git a/app/javascript/dashboard/components/SettingsSection.vue b/app/javascript/dashboard/components/SettingsSection.vue index adf6f9912..fd930914b 100644 --- a/app/javascript/dashboard/components/SettingsSection.vue +++ b/app/javascript/dashboard/components/SettingsSection.vue @@ -1,6 +1,6 @@ + + diff --git a/app/javascript/dashboard/store/modules/inboxes.js b/app/javascript/dashboard/store/modules/inboxes.js index 8b5e48ac6..6fd7aae2d 100644 --- a/app/javascript/dashboard/store/modules/inboxes.js +++ b/app/javascript/dashboard/store/modules/inboxes.js @@ -139,6 +139,14 @@ export const actions = { throw new Error(error); } }, + reauthorizeFacebookPage: async ({ commit }, params) => { + try { + const response = await FBChannel.reauthorizeFacebookPage(params); + commit(types.default.EDIT_INBOXES, response.data); + } catch (error) { + throw new Error(error.message); + } + }, }; export const mutations = { diff --git a/app/models/channel/facebook_page.rb b/app/models/channel/facebook_page.rb index 80dbf0e6d..8c0ba1b9c 100644 --- a/app/models/channel/facebook_page.rb +++ b/app/models/channel/facebook_page.rb @@ -19,6 +19,8 @@ class Channel::FacebookPage < ApplicationRecord self.table_name = 'channel_facebook_pages' + include Reauthorizable + validates :account_id, presence: true validates :page_id, uniqueness: { scope: :account_id } belongs_to :account diff --git a/app/models/concerns/reauthorizable.rb b/app/models/concerns/reauthorizable.rb new file mode 100644 index 000000000..e20e24bd8 --- /dev/null +++ b/app/models/concerns/reauthorizable.rb @@ -0,0 +1,57 @@ +# This concern is primarily targetted for business models dependant on external services +# The auth tokens we obtained on their behalf could expire or becomes invalid. +# We would be aware of it until we make the API call to the service and it throws error + +# Example: +# when a user changes his/her password, the auth token they provided to chatwoot becomes invalid + +# This module helps to capture the errors into a counter and when threshold is passed would mark +# the object to be reauthorized. We will also send an email to the owners alerting them of the error. + +# In the UI, we will check for the reauthorization_required? status and prompt the reauthorization flow + +module Reauthorizable + extend ActiveSupport::Concern + + AUTHORIZATION_ERROR_THRESHOLD = 2 + + # model attribute + def reauthorization_required? + ::Redis::Alfred.get(reauthorization_required_key).present? + end + + # model attribute + def authorization_error_count + ::Redis::Alfred.get(authorization_error_count_key).to_i + end + + # action to be performed when we recieve authorization errors + # Implement in your exception handling logic for authorization errors + def authorization_error! + ::Redis::Alfred.incr(authorization_error_count_key) + prompt_reauthorization! if authorization_error_count >= AUTHORIZATION_ERROR_THRESHOLD + end + + # Performed automatically if error threshold is breached + # could used to manually prompt reauthorization if auth scope changes + def prompt_reauthorization! + ::Redis::Alfred.set(reauthorization_required_key, true) + # TODO: send_reauthorize_prompt_email + end + + # call this after you successfully Reauthorized the object in UI + def reauthorized! + ::Redis::Alfred.delete(authorization_error_count_key) + ::Redis::Alfred.delete(reauthorization_required_key) + end + + private + + def authorization_error_count_key + format(::Redis::Alfred::AUTHORIZATION_ERROR_COUNT, obj_type: self.class.table_name.singularize, obj_id: id) + end + + def reauthorization_required_key + format(::Redis::Alfred::REAUTHORIZATION_REQUIRED, obj_type: self.class.table_name.singularize, obj_id: id) + end +end diff --git a/app/services/facebook/send_on_facebook_service.rb b/app/services/facebook/send_on_facebook_service.rb index 629e680a6..fdb6d1069 100644 --- a/app/services/facebook/send_on_facebook_service.rb +++ b/app/services/facebook/send_on_facebook_service.rb @@ -6,7 +6,11 @@ class Facebook::SendOnFacebookService < Base::SendOnChannelService end def perform_reply - FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token) + result = FacebookBot::Bot.deliver(delivery_params, access_token: message.channel_token) + message.update!(source_id: JSON.parse(result)['message_id']) + rescue Facebook::Messenger::FacebookError => e + Rails.logger.info e + channel.authorization_error! end def fb_text_message_params diff --git a/app/views/api/v1/accounts/callbacks/reauthorize_page.json.jbuilder b/app/views/api/v1/accounts/callbacks/reauthorize_page.json.jbuilder new file mode 100644 index 000000000..3bc362380 --- /dev/null +++ b/app/views/api/v1/accounts/callbacks/reauthorize_page.json.jbuilder @@ -0,0 +1,3 @@ +json.data do + json.partial! 'api/v1/models/inbox.json.jbuilder', resource: @inbox +end diff --git a/app/views/api/v1/models/_inbox.json.jbuilder b/app/views/api/v1/models/_inbox.json.jbuilder index 136ea1ea1..44d75ee22 100644 --- a/app/views/api/v1/models/_inbox.json.jbuilder +++ b/app/views/api/v1/models/_inbox.json.jbuilder @@ -15,3 +15,4 @@ json.web_widget_script resource.channel.try(:web_widget_script) json.forward_to_address resource.channel.try(:forward_to_address) json.phone_number resource.channel.try(:phone_number) json.selected_feature_flags resource.channel.try(:selected_feature_flags) +json.reauthorization_required resource.channel.try(:reauthorization_required?) if resource.facebook? diff --git a/lib/redis/alfred.rb b/lib/redis/alfred.rb index d023f9fa3..ca02eb936 100644 --- a/lib/redis/alfred.rb +++ b/lib/redis/alfred.rb @@ -1,13 +1,17 @@ +# refer : https://redis.io/commands + module Redis::Alfred include Redis::RedisKeys class << self # key operations + # set a value in redis def set(key, value) $alfred.set(key, value) end + # set a key with expiry period def setex(key, value, expiry = 1.day) $alfred.setex(key, expiry, value) end @@ -20,6 +24,12 @@ module Redis::Alfred $alfred.del(key) end + # increment a key by 1. throws error if key value is incompatible + # sets key to 0 before operation if key doesn't exist + def incr(key) + $alfred.incr(key) + end + # list operations def llen(key) diff --git a/lib/redis/redis_keys.rb b/lib/redis/redis_keys.rb index 417978d7c..78a10e277 100644 --- a/lib/redis/redis_keys.rb +++ b/lib/redis/redis_keys.rb @@ -10,4 +10,8 @@ module Redis::RedisKeys ONLINE_PRESENCE_CONTACTS = 'ONLINE_PRESENCE::%d::CONTACTS'.freeze # sorted set storing online presense of account users ONLINE_PRESENCE_USERS = 'ONLINE_PRESENCE::%d::USERS'.freeze + + ## Authorization Status Keys + AUTHORIZATION_ERROR_COUNT = 'AUTHORIZATION_ERROR_COUNT:%s:%d'.freeze + REAUTHORIZATION_REQUIRED = 'REAUTHORIZATION_REQUIRED:%s:%d'.freeze end diff --git a/spec/controllers/api/v1/accounts/callbacks_controller_spec.rb b/spec/controllers/api/v1/accounts/callbacks_controller_spec.rb index d3c017607..1df26b264 100644 --- a/spec/controllers/api/v1/accounts/callbacks_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/callbacks_controller_spec.rb @@ -114,6 +114,7 @@ RSpec.describe 'Callbacks API', type: :request do as: :json expect(response).to have_http_status(:success) + expect(response.body).to include(inbox.id.to_s) end it 'returns unprocessable_entity if no page found' do diff --git a/spec/models/channel/facebook_page_spec.rb b/spec/models/channel/facebook_page_spec.rb index c31f466d5..309cc74dc 100644 --- a/spec/models/channel/facebook_page_spec.rb +++ b/spec/models/channel/facebook_page_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'rails_helper' +require Rails.root.join 'spec/models/concerns/reauthorizable_spec.rb' RSpec.describe Channel::FacebookPage do let(:channel) { create(:channel_facebook_page) } @@ -10,6 +11,10 @@ RSpec.describe Channel::FacebookPage do it { is_expected.to belong_to(:account) } it { is_expected.to have_one(:inbox).dependent(:destroy) } + describe 'concerns' do + it_behaves_like 'reauthorizable' + end + it 'has a valid name' do expect(channel.name).to eq('Facebook') end diff --git a/spec/models/concerns/reauthorizable_spec.rb b/spec/models/concerns/reauthorizable_spec.rb new file mode 100644 index 000000000..62b251367 --- /dev/null +++ b/spec/models/concerns/reauthorizable_spec.rb @@ -0,0 +1,38 @@ +require 'rails_helper' + +shared_examples_for 'reauthorizable' do + let(:model) { described_class } # the class that includes the concern + + it 'authorization_error!' do + obj = FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym) + expect(obj.authorization_error_count).to eq 0 + + obj.authorization_error! + + expect(obj.authorization_error_count).to eq 1 + end + + it 'prompt_reauthorization!' do + obj = FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym) + expect(obj.reauthorization_required?).to eq false + + obj.prompt_reauthorization! + + expect(obj.reauthorization_required?).to eq true + end + + it 'reauthorized!' do + obj = FactoryBot.create(model.to_s.underscore.tr('/', '_').to_sym) + # setting up the object with the errors to validate its cleared on action + obj.authorization_error! + obj.prompt_reauthorization! + expect(obj.reauthorization_required?).to eq true + expect(obj.authorization_error_count).not_to eq 0 + + obj.reauthorized! + + # authorization errors are reset + expect(obj.authorization_error_count).to eq 0 + expect(obj.reauthorization_required?).to eq false + end +end diff --git a/spec/services/facebook/send_on_facebook_service_spec.rb b/spec/services/facebook/send_on_facebook_service_spec.rb index 25a84f9e7..a972a8973 100644 --- a/spec/services/facebook/send_on_facebook_service_spec.rb +++ b/spec/services/facebook/send_on_facebook_service_spec.rb @@ -5,7 +5,7 @@ describe Facebook::SendOnFacebookService do before do allow(Facebook::Messenger::Subscriptions).to receive(:subscribe).and_return(true) - allow(bot).to receive(:deliver) + allow(bot).to receive(:deliver).and_return({ recipient_id: '1008372609250235', message_id: 'mid.1456970487936:c34767dfe57ee6e339' }.to_json) create(:message, message_type: :incoming, inbox: facebook_inbox, account: account, conversation: conversation) end