Merge pull request #891 from Retrospring/revoke-twitter-on-unauthorized
Revoke Twitter connection when the token is revoked
This commit is contained in:
commit
ca39d42e18
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
class ServicesController < ApplicationController
|
class ServicesController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
before_action :mark_notifications_as_read, only: %i[index]
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@services = current_user.services
|
@services = current_user.services
|
||||||
|
@ -59,4 +60,10 @@ class ServicesController < ApplicationController
|
||||||
def omniauth_hash
|
def omniauth_hash
|
||||||
request.env["omniauth.auth"]
|
request.env["omniauth.auth"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def mark_notifications_as_read
|
||||||
|
Notification::ServiceTokenExpired
|
||||||
|
.where(recipient: current_user, new: true)
|
||||||
|
.update_all(new: false) # rubocop:disable Rails/SkipsModelValidations
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
&__user,
|
&__user,
|
||||||
&__text {
|
&__text {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__icon {
|
&__icon {
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Notification::ServiceTokenExpired < Notification
|
||||||
|
end
|
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class Notification::TwitterTokenExpired < Notification::ServiceTokenExpired
|
||||||
|
end
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
# stub model for notifying about expired service connections
|
||||||
|
class User::ExpiredServiceConnection < User
|
||||||
|
end
|
|
@ -0,0 +1,4 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class User::ExpiredTwitterServiceConnection < User::ExpiredServiceConnection
|
||||||
|
end
|
|
@ -0,0 +1,8 @@
|
||||||
|
.media.notification
|
||||||
|
.notification__icon
|
||||||
|
%i.fa.fa-2x.fa-fw.fa-twitter
|
||||||
|
.media-body
|
||||||
|
%h6.media-heading.notification__user
|
||||||
|
= t(".heading")
|
||||||
|
.notification__text
|
||||||
|
= t(".text_html", settings_sharing: link_to(t(".settings_services"), services_path))
|
|
@ -8,11 +8,10 @@ class ShareWorker
|
||||||
# @param user_id [Integer] the user id
|
# @param user_id [Integer] the user id
|
||||||
# @param answer_id [Integer] the user id
|
# @param answer_id [Integer] the user id
|
||||||
# @param service [String] the service to post to
|
# @param service [String] the service to post to
|
||||||
def perform(user_id, answer_id, service)
|
def perform(user_id, answer_id, service) # rubocop:disable Metrics/AbcSize
|
||||||
service_type = "Services::#{service.camelize}"
|
@user_service = User.find(user_id).services.find_by(type: "Services::#{service.camelize}")
|
||||||
user_service = User.find(user_id).services.find_by(type: service_type)
|
|
||||||
|
|
||||||
user_service.post(Answer.find(answer_id))
|
@user_service.post(Answer.find(answer_id))
|
||||||
rescue ActiveRecord::RecordNotFound
|
rescue ActiveRecord::RecordNotFound
|
||||||
logger.info "Tried to post answer ##{answer_id} for user ##{user_id} to #{service.titleize} but the user/answer/service did not exist (likely deleted), will not retry."
|
logger.info "Tried to post answer ##{answer_id} for user ##{user_id} to #{service.titleize} but the user/answer/service did not exist (likely deleted), will not retry."
|
||||||
# The question to be posted was deleted
|
# The question to be posted was deleted
|
||||||
|
@ -23,11 +22,22 @@ class ShareWorker
|
||||||
logger.info "Tried to post answer ##{answer_id} from user ##{user_id} to Twitter but the account is suspended."
|
logger.info "Tried to post answer ##{answer_id} from user ##{user_id} to Twitter but the account is suspended."
|
||||||
rescue Twitter::Error::Unauthorized
|
rescue Twitter::Error::Unauthorized
|
||||||
# User's Twitter token has expired or been revoked
|
# User's Twitter token has expired or been revoked
|
||||||
# TODO: Notify user if this happens (https://github.com/Retrospring/retrospring/issues/123)
|
logger.info "Tried to post answer ##{answer_id} from user ##{user_id} to Twitter but the token has expired or been revoked."
|
||||||
logger.info "Tried to post answer ##{answer_id} from user ##{user_id} to Twitter but the token has exired or been revoked."
|
revoke_and_notify(user_id, service)
|
||||||
rescue => e
|
rescue => e
|
||||||
logger.info "failed to post answer #{answer_id} to #{service} for user #{user_id}: #{e.message}"
|
logger.info "failed to post answer #{answer_id} to #{service} for user #{user_id}: #{e.message}"
|
||||||
Sentry.capture_exception(e)
|
Sentry.capture_exception(e)
|
||||||
raise
|
raise
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def revoke_and_notify(user_id, service)
|
||||||
|
@user_service.destroy
|
||||||
|
|
||||||
|
Notification::ServiceTokenExpired.create(
|
||||||
|
target_id: user_id,
|
||||||
|
target_type: "User::Expired#{service.camelize}ServiceConnection",
|
||||||
|
recipient_id: user_id,
|
||||||
|
new: true
|
||||||
|
)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -352,6 +352,10 @@ en:
|
||||||
link_text: "your comment"
|
link_text: "your comment"
|
||||||
follow:
|
follow:
|
||||||
heading_html: "followed you %{time} ago"
|
heading_html: "followed you %{time} ago"
|
||||||
|
expiredtwitterserviceconnection:
|
||||||
|
heading: "Twitter connection expired"
|
||||||
|
text_html: "If you would like to continue automatically sharing your answers to Twitter, head to %{settings_sharing} and re-connect your account."
|
||||||
|
settings_services: "Sharing Settings"
|
||||||
services:
|
services:
|
||||||
index:
|
index:
|
||||||
title: "Service Settings"
|
title: "Service Settings"
|
||||||
|
|
|
@ -1,17 +1,65 @@
|
||||||
require 'rails_helper'
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require "rails_helper"
|
||||||
|
|
||||||
describe ServicesController, type: :controller do
|
describe ServicesController, type: :controller do
|
||||||
context 'successful Twitter sign in' do
|
describe "#index" do
|
||||||
|
subject { get :index }
|
||||||
|
|
||||||
|
context "user signed in" do
|
||||||
|
let(:user) { FactoryBot.create(:user) }
|
||||||
|
|
||||||
|
before { sign_in user }
|
||||||
|
|
||||||
|
it "renders the services settings page with no services" do
|
||||||
|
subject
|
||||||
|
expect(response).to render_template("index")
|
||||||
|
expect(controller.instance_variable_get(:@services)).to be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
context "user has a service token expired notification" do
|
||||||
|
let(:notification) do
|
||||||
|
Notification::ServiceTokenExpired.create(
|
||||||
|
target_id: user.id,
|
||||||
|
target_type: "User::ExpiredTwitterServiceConnection",
|
||||||
|
recipient_id: user.id,
|
||||||
|
new: true
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "marks the notification as read" do
|
||||||
|
expect { subject }.to change { notification.reload.new }.from(true).to(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "user has Twitter connected" do
|
||||||
|
before do
|
||||||
|
Services::Twitter.create(user:, uid: 12)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "renders the services settings page" do
|
||||||
|
subject
|
||||||
|
expect(response).to render_template("index")
|
||||||
|
expect(controller.instance_variable_get(:@services)).not_to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#create" do
|
||||||
|
subject { get :create, params: { provider: "twitter" } }
|
||||||
|
|
||||||
|
context "successful Twitter sign in" do
|
||||||
let(:user) { FactoryBot.create(:user) }
|
let(:user) { FactoryBot.create(:user) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
sign_in user
|
sign_in user
|
||||||
OmniAuth.config.test_mode = true
|
OmniAuth.config.test_mode = true
|
||||||
OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({
|
OmniAuth.config.mock_auth[:twitter] = OmniAuth::AuthHash.new({
|
||||||
'provider' => 'twitter',
|
"provider" => "twitter",
|
||||||
'uid' => '12',
|
"uid" => "12",
|
||||||
'info' => { 'nickname' => 'jack' },
|
"info" => { "nickname" => "jack" },
|
||||||
'credentials' => { 'token' => 'AAAA', 'secret' => 'BBBB' }
|
"credentials" => { "token" => "AAAA", "secret" => "BBBB" }
|
||||||
})
|
})
|
||||||
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
|
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:twitter]
|
||||||
end
|
end
|
||||||
|
@ -20,58 +68,57 @@ describe ServicesController, type: :controller do
|
||||||
OmniAuth.config.mock_auth[:twitter] = nil
|
OmniAuth.config.mock_auth[:twitter] = nil
|
||||||
end
|
end
|
||||||
|
|
||||||
subject { get :create, params: { provider: 'twitter' } }
|
context "no services connected" do
|
||||||
|
it "creates a service integration" do
|
||||||
context 'no services connected' do
|
|
||||||
it 'creates a service integration' do
|
|
||||||
expect { subject }.to change { Service.count }.by(1)
|
expect { subject }.to change { Service.count }.by(1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'a user has a service connected' do
|
context "a user has a service connected" do
|
||||||
let(:other_user) { FactoryBot.create(:user) }
|
let(:other_user) { FactoryBot.create(:user) }
|
||||||
let!(:service) { Services::Twitter.create(user: other_user, uid: 12) }
|
let!(:service) { Services::Twitter.create(user: other_user, uid: 12) }
|
||||||
|
|
||||||
it 'shows an error when trying to attach a service account which is already connected' do
|
it "shows an error when trying to attach a service account which is already connected" do
|
||||||
subject
|
subject
|
||||||
expect(flash[:error]).to eq("The Twitter account you are trying to connect is already connected to another #{APP_CONFIG['site_name']} account. If you are unable to disconnect the account yourself, please send us a Direct Message on Twitter: @retrospring.")
|
expect(flash[:error]).to eq("The Twitter account you are trying to connect is already connected to another #{APP_CONFIG['site_name']} account. If you are unable to disconnect the account yourself, please send us a Direct Message on Twitter: @retrospring.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context '#update' do
|
describe "#update" do
|
||||||
subject { patch :update, params: params }
|
subject { patch :update, params: }
|
||||||
|
|
||||||
context 'not signed in' do
|
context "not signed in" do
|
||||||
let(:params) { { id: 1 } }
|
let(:params) { { id: 1 } }
|
||||||
|
|
||||||
it 'redirects to sign in page' do
|
it "redirects to sign in page" do
|
||||||
subject
|
subject
|
||||||
expect(response).to redirect_to(new_user_session_path)
|
expect(response).to redirect_to(new_user_session_path)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'user with Twitter connection' do
|
context "user with Twitter connection" do
|
||||||
before { sign_in user }
|
before { sign_in user }
|
||||||
|
|
||||||
let(:user) { FactoryBot.create(:user) }
|
let(:user) { FactoryBot.create(:user) }
|
||||||
let(:service) { Services::Twitter.create(user: user, uid: 12) }
|
let(:service) { Services::Twitter.create(user:, uid: 12) }
|
||||||
let(:params) { { id: service.id, service: { post_tag: post_tag } } }
|
let(:params) { { id: service.id, service: { post_tag: } } }
|
||||||
|
|
||||||
context 'tag is valid' do
|
context "tag is valid" do
|
||||||
let(:post_tag) { '#askaraccoon' }
|
let(:post_tag) { "#askaraccoon" }
|
||||||
|
|
||||||
it 'updates a service connection' do
|
it "updates a service connection" do
|
||||||
expect { subject }.to change { service.reload.post_tag }.to('#askaraccoon')
|
expect { subject }.to change { service.reload.post_tag }.to("#askaraccoon")
|
||||||
expect(response).to redirect_to(services_path)
|
expect(response).to redirect_to(services_path)
|
||||||
expect(flash[:success]).to eq("Service updated successfully.")
|
expect(flash[:success]).to eq("Service updated successfully.")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'tag is too long' do
|
context "tag is too long" do
|
||||||
let(:post_tag) { 'a' * 21 } # 1 character over the limit
|
let(:post_tag) { "a" * 21 } # 1 character over the limit
|
||||||
|
|
||||||
it 'shows an error' do
|
it "shows an error" do
|
||||||
subject
|
subject
|
||||||
expect(response).to redirect_to(services_path)
|
expect(response).to redirect_to(services_path)
|
||||||
expect(flash[:error]).to eq("Unable to update service.")
|
expect(flash[:error]).to eq("Unable to update service.")
|
||||||
|
|
|
@ -56,7 +56,19 @@ describe ShareWorker do
|
||||||
allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Unauthorized)
|
allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Unauthorized)
|
||||||
subject
|
subject
|
||||||
ShareWorker.drain
|
ShareWorker.drain
|
||||||
expect(Sidekiq.logger).to have_received(:info).with("Tried to post answer ##{answer.id} from user ##{user.id} to Twitter but the token has exired or been revoked.")
|
expect(Sidekiq.logger).to have_received(:info).with("Tried to post answer ##{answer.id} from user ##{user.id} to Twitter but the token has expired or been revoked.")
|
||||||
|
end
|
||||||
|
|
||||||
|
it "revokes the service connection when Twitter::Error::Unauthorized is raised" do
|
||||||
|
allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Unauthorized)
|
||||||
|
subject
|
||||||
|
expect { ShareWorker.drain }.to change { Services::Twitter.count }.by(-1)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "sends the user a notification when Twitter::Error::Unauthorized is raised" do
|
||||||
|
allow_any_instance_of(Services::Twitter).to receive(:post).with(answer).and_raise(Twitter::Error::Unauthorized)
|
||||||
|
subject
|
||||||
|
expect { ShareWorker.drain }.to change { Notification::ServiceTokenExpired.count }.by(1)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "handles Twitter::Error::Forbidden" do
|
it "handles Twitter::Error::Forbidden" do
|
||||||
|
|
Loading…
Reference in New Issue