Feature: SignIn with Twitter (#479)
* Add Twitter SignIn flow Co-authored-by: Sojan Jose <sojan@pepalo.com>
This commit is contained in:
parent
272c481464
commit
30f4c08143
14 changed files with 249 additions and 3 deletions
|
@ -1,6 +1,6 @@
|
|||
GIT
|
||||
remote: https://github.com/chatwoot/twitty
|
||||
revision: 58b4958d7f4a58eec8fe9543caedb232308253f6
|
||||
revision: c1edd557401d1e8a197b19e738f82e39507a8e2d
|
||||
specs:
|
||||
twitty (0.1.0)
|
||||
oauth
|
||||
|
|
30
app/controllers/twitter/authorizations_controller.rb
Normal file
30
app/controllers/twitter/authorizations_controller.rb
Normal 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
|
24
app/controllers/twitter/base_controller.rb
Normal file
24
app/controllers/twitter/base_controller.rb
Normal 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
|
51
app/controllers/twitter/callbacks_controller.rb
Normal file
51
app/controllers/twitter/callbacks_controller.rb
Normal 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
|
|
@ -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)) {
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
20
app/services/twitter/webhook_subscribe_service.rb
Normal file
20
app/services/twitter/webhook_subscribe_service.rb
Normal 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
|
|
@ -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]
|
||||
|
|
33
spec/controllers/twitter/callbacks_controller_spec.rb
Normal file
33
spec/controllers/twitter/callbacks_controller_spec.rb
Normal 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
|
36
spec/services/twitter/webhook_subscribe_service_spec.rb
Normal file
36
spec/services/twitter/webhook_subscribe_service_spec.rb
Normal 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
|
Loading…
Add table
Add a link
Reference in a new issue