Feature: SignIn with Twitter (#479)

* Add Twitter SignIn flow

Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
Pranav Raj S 2020-02-11 14:27:38 +05:30 committed by GitHub
parent 272c481464
commit 30f4c08143
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 249 additions and 3 deletions

View file

@ -1,6 +1,6 @@
GIT
remote: https://github.com/chatwoot/twitty
revision: 58b4958d7f4a58eec8fe9543caedb232308253f6
revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
specs:
twitty (0.1.0)
oauth

View file

@ -0,0 +1,30 @@
class Twitter::AuthorizationsController < Twitter::BaseController
def create
@response = twitter_client.request_oauth_token(url: twitter_callback_url)
if @response.status == '200'
::Redis::Alfred.setex(oauth_token, account.id)
redirect_to oauth_authorize_endpoint(oauth_token)
else
redirect_to app_new_twitter_inbox_url
end
end
private
def oauth_token
parsed_body['oauth_token']
end
def user
@user ||= User.find_by(id: params[:user_id])
end
def account
@account ||= user.account
end
def oauth_authorize_endpoint(oauth_token)
"#{twitter_api_base_url}/oauth/authorize?oauth_token=#{oauth_token}"
end
end

View file

@ -0,0 +1,24 @@
class Twitter::BaseController < ApplicationController
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def host
ENV.fetch('FRONTEND_URL', '')
end
def twitter_client
Twitty::Facade.new do |config|
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
config.base_url = twitter_api_base_url
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
end
end
def twitter_api_base_url
'https://api.twitter.com'
end
end

View file

@ -0,0 +1,51 @@
class Twitter::CallbacksController < Twitter::BaseController
def show
@response = twitter_client.access_token(
oauth_token: permitted_params[:oauth_token],
oauth_verifier: permitted_params[:oauth_verifier]
)
if @response.status == '200'
inbox = build_inbox
::Redis::Alfred.delete(permitted_params[:oauth_token])
::Twitter::WebhookSubscribeService.new(inbox_id: inbox.id).perform
redirect_to app_twitter_inbox_agents_url(inbox_id: inbox.id)
else
redirect_to app_new_twitter_inbox_url
end
end
private
def parsed_body
@parsed_body ||= Rack::Utils.parse_nested_query(@response.raw_response.body)
end
def account_id
::Redis::Alfred.get(permitted_params[:oauth_token])
end
def account
@account ||= Account.find_by!(id: account_id)
end
def build_inbox
ActiveRecord::Base.transaction do
twitter_profile = account.twitter_profiles.create(
twitter_access_token: parsed_body['oauth_token'],
twitter_access_token_secret: parsed_body['oauth_token_secret'],
profile_id: parsed_body['user_id'],
name: parsed_body['screen_name']
)
account.inboxes.create(
name: parsed_body['screen_name'],
channel: twitter_profile
)
rescue StandardError => e
Rails.logger e
end
end
def permitted_params
params.permit(:oauth_token, :oauth_verifier)
end
end

View file

@ -39,7 +39,7 @@ export default {
},
methods: {
isActive(channel) {
return ['facebook', 'website'].includes(channel);
return ['facebook', 'website', 'twitter'].includes(channel);
},
onItemClick() {
if (this.isActive(this.channel)) {

View file

@ -15,6 +15,9 @@
"FB": {
"HELP": "PS: By signing in, we only get access to your Page's messages. Your private messages can never be accessed by Chatwoot."
},
"TWITTER": {
"HELP": "To add your Twitter profile as a channel, you need to authenticate your Twitter Profile by clicking on 'Sign in with Twitter' "
},
"WEBSITE_CHANNEL": {
"TITLE": "Website channel",
"DESC": "Create a channel for your website and start supporting your customers via our website widget.",

View file

@ -1,9 +1,11 @@
import Facebook from './channels/Facebook';
import Website from './channels/Website';
import Twitter from './channels/Twitter';
const channelViewList = {
facebook: Facebook,
website: Website,
twitter: Twitter,
};
export default {

View file

@ -0,0 +1,27 @@
<template>
<div class="wizard-body columns content-box small-9">
<div class="login-init full-height text-center">
<form method="POST" action="/twitter/authorization">
<input type="hidden" name="user_id" :value="currentUserID" />
<woot-submit-button
icon-class="ion-social-twitter"
button-text="Sign in with Twitter"
type="submit"
/>
</form>
<p>{{ $t('INBOX_MGMT.ADD.TWITTER.HELP') }}</p>
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters({
currentUserID: 'getCurrentUserID',
}),
},
};
</script>

View file

@ -18,6 +18,7 @@ class Account < ApplicationRecord
has_many :conversations, dependent: :destroy
has_many :contacts, dependent: :destroy
has_many :facebook_pages, dependent: :destroy, class_name: '::Channel::FacebookPage'
has_many :twitter_profiles, dependent: :destroy, class_name: '::Channel::TwitterProfile'
has_many :web_widgets, dependent: :destroy, class_name: '::Channel::WebWidget'
has_many :telegram_bots, dependent: :destroy
has_many :canned_responses, dependent: :destroy

View file

@ -41,9 +41,21 @@ class Channel::TwitterProfile < ApplicationRecord
end
end
def twitter_client
Twitty::Facade.new do |config|
config.consumer_key = ENV.fetch('TWITTER_CONSUMER_KEY', nil)
config.consumer_secret = ENV.fetch('TWITTER_CONSUMER_SECRET', nil)
config.access_token = twitter_access_token
config.access_token_secret = twitter_access_token_secret
config.base_url = 'https://api.twitter.com'
config.environment = ENV.fetch('TWITTER_ENVIRONMENT', '')
end
end
private
def unsubscribe
# to implement
webhooks_list = twitter_client.fetch_webhooks.body
twitter_client.unsubscribe_webhook(id: webhooks_list.first['id']) if webhooks_list.present?
end
end

View file

@ -0,0 +1,20 @@
class Twitter::WebhookSubscribeService
include Rails.application.routes.url_helpers
pattr_initialize [:inbox_id]
def perform
register_response = twitter_client.register_webhook(url: webhooks_twitter_url)
twitter_client.subscribe_webhook if register_response.status == '200'
Rails.logger.info 'TWITTER_REGISTER_WEBHOOK_FAILURE: ' + register_response.body.to_s
end
private
delegate :channel, to: :inbox
delegate :twitter_client, to: :channel
def inbox
Inbox.find_by!(id: inbox_id)
end
end

View file

@ -12,6 +12,8 @@ Rails.application.routes.draw do
get '/app', to: 'dashboard#index'
get '/app/*params', to: 'dashboard#index'
get '/app/settings/inboxes/new/twitter', to: 'dashboard#index', as: 'app_new_twitter_inbox'
get '/app/settings/inboxes/new/:inbox_id/agents', to: 'dashboard#index', as: 'app_twitter_inbox_agents'
match '/status', to: 'home#status', via: [:get]
@ -104,6 +106,11 @@ Rails.application.routes.draw do
end
end
namespace :twitter do
resource :authorization, only: [:create]
resource :callback, only: [:show]
end
# Used in mailer templates
resource :app, only: [:index] do
resources :conversations, only: [:show]

View file

@ -0,0 +1,33 @@
require 'rails_helper'
RSpec.describe 'Twitter::CallbacksController', type: :request do
subject(:webhook_subscribe_service) { described_class.new(inbox_id: twitter_inbox.id) }
let(:twitter_client) { instance_double(::Twitty::Facade) }
let(:twitter_response) { instance_double(::Twitty::Response, status: '200', body: { message: 'Valid' }) }
let(:raw_response) do
object_double('raw_response', body: 'oauth_token=1&oauth_token_secret=1&user_id=100&screen_name=chatwoot')
end
let(:account) { create(:account) }
before do
allow(::Twitty::Facade).to receive(:new).and_return(twitter_client)
allow(::Redis::Alfred).to receive(:get).and_return(account.id)
allow(::Redis::Alfred).to receive(:delete).and_return('OK')
allow(twitter_client).to receive(:access_token).and_return(twitter_response)
allow(twitter_client).to receive(:register_webhook).and_return(twitter_response)
allow(twitter_client).to receive(:subscribe_webhook).and_return(true)
allow(twitter_response).to receive(:raw_response).and_return(raw_response)
end
describe 'GET /twitter/callback' do
it 'renders the page correctly when called with website_token' do
get twitter_callback_url
account.reload
expect(response).to redirect_to app_twitter_inbox_agents_url(inbox_id: account.inboxes.last.id)
expect(account.inboxes.count).to be 1
expect(account.twitter_profiles.last.inbox.name).to eq 'chatwoot'
expect(account.twitter_profiles.last.profile_id).to eq '100'
end
end
end

View file

@ -0,0 +1,36 @@
require 'rails_helper'
describe ::Twitter::WebhookSubscribeService do
subject(:webhook_subscribe_service) { described_class.new(inbox_id: twitter_inbox.id) }
let(:twitter_client) { instance_double(::Twitty::Facade) }
let(:twitter_success_response) { instance_double(::Twitty::Response, status: '200', body: { message: 'Valid' }) }
let(:twitter_error_response) { instance_double(::Twitty::Response, status: '422', body: { message: 'Invalid request' }) }
let(:account) { create(:account) }
let(:twitter_channel) { create(:channel_twitter_profile, account: account) }
let(:twitter_inbox) { create(:inbox, channel: twitter_channel, account: account) }
before do
allow(::Twitty::Facade).to receive(:new).and_return(twitter_client)
allow(twitter_client).to receive(:register_webhook)
allow(twitter_client).to receive(:subscribe_webhook)
end
describe '#perform' do
context 'with successful registration' do
it 'calls subscribe webhook' do
allow(twitter_client).to receive(:register_webhook).and_return(twitter_success_response)
webhook_subscribe_service.perform
expect(twitter_client).to have_received(:subscribe_webhook)
end
end
context 'with unsuccessful registration' do
it 'does not call subscribe webhook' do
allow(twitter_client).to receive(:register_webhook).and_return(twitter_error_response)
webhook_subscribe_service.perform
expect(twitter_client).not_to have_received(:subscribe_webhook)
end
end
end
end