Feature: Access tokens for API access (#604)

Co-authored-by: Pranav Raj Sreepuram <pranavrajs@gmail.com>
This commit is contained in:
Sojan Jose
2020-03-11 00:02:15 +05:30
committed by GitHub
parent 19ab0fe108
commit a5b1e2b650
29 changed files with 517 additions and 270 deletions

View File

@@ -22,7 +22,7 @@ class Messages::Outgoing::NormalBuilder
message_type: :outgoing, message_type: :outgoing,
content: @content, content: @content,
private: @private, private: @private,
user_id: @user.id, user_id: @user&.id,
source_id: @fb_id source_id: @fb_id
} }
end end

View File

@@ -1,9 +1,16 @@
class Api::BaseController < ApplicationController class Api::BaseController < ApplicationController
include AccessTokenAuthHelper
respond_to :json respond_to :json
before_action :authenticate_user! before_action :authenticate_access_token!, if: :authenticate_by_access_token?
before_action :validate_bot_access_token!, if: :authenticate_by_access_token?
before_action :authenticate_user!, unless: :authenticate_by_access_token?
private private
def authenticate_by_access_token?
request.headers[:api_access_token].present?
end
def set_conversation def set_conversation
@conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id]) @conversation ||= current_account.conversations.find_by(display_id: params[:conversation_id])
end end

View File

@@ -1,4 +1,4 @@
class Api::V1::Widget::InboxesController < Api::BaseController class Api::V1::Accounts::Widget::InboxesController < Api::BaseController
before_action :authorize_request before_action :authorize_request
before_action :set_web_widget_channel, only: [:update] before_action :set_web_widget_channel, only: [:update]
before_action :set_inbox, only: [:update] before_action :set_inbox, only: [:update]

View File

@@ -14,7 +14,25 @@ class ApplicationController < ActionController::Base
private private
def current_account def current_account
@_ ||= current_user.account @_ ||= find_current_account
end
def find_current_account
account = Account.find(params[:account_id])
if current_user
account_accessible_for_user?(account)
elsif @resource&.is_a?(AgentBot)
account_accessible_for_bot?(account)
end
account
end
def account_accessible_for_user?(account)
render_unauthorized('You are not authorized to access this account') unless account.account_users.find_by(user_id: current_user.id)
end
def account_accessible_for_bot?(account)
render_unauthorized('You are not authorized to access this account') unless @resource.agent_bot_inboxes.find_by(account_id: account.id)
end end
def handle_with_exception def handle_with_exception

View File

@@ -0,0 +1,24 @@
module AccessTokenAuthHelper
BOT_ACCESSIBLE_ENDPOINTS = {
'api/v1/accounts/conversations' => ['toggle_status'],
'api/v1/accounts/conversations/messages' => ['create']
}.freeze
def authenticate_access_token!
access_token = AccessToken.find_by(token: request.headers[:api_access_token])
render_unauthorized('Invalid Access Token') && return unless access_token
token_owner = access_token.owner
@resource = token_owner
end
def validate_bot_access_token!
return if current_user.is_a?(User)
return if agent_bot_accessible?
render_unauthorized('Access to this endpoint is not authorized for bots')
end
def agent_bot_accessible?
BOT_ACCESSIBLE_ENDPOINTS.fetch(params[:controller], []).include?(params[:action])
end
end

View File

@@ -2,7 +2,7 @@ import ApiClient from '../ApiClient';
class WebChannel extends ApiClient { class WebChannel extends ApiClient {
constructor() { constructor() {
super('widget/inboxes'); super('widget/inboxes', { accountScoped: true });
} }
} }

View File

@@ -26,7 +26,8 @@ export default {
}, },
}, },
methods: { methods: {
onCopy() { onCopy(e) {
e.preventDefault();
copy(this.script); copy(this.script);
bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL')); bus.$emit('newToastMessage', this.$t('COMPONENTS.CODE.COPY_SUCCESSFUL'));
}, },

View File

@@ -18,6 +18,10 @@
"TITLE": "Password", "TITLE": "Password",
"NOTE": "Updating your password would reset your logins in multiple devices." "NOTE": "Updating your password would reset your logins in multiple devices."
}, },
"ACCESS_TOKEN": {
"TITLE": "Access Token",
"NOTE": "This token can be used if you are building an API based integration"
},
"EMAIL_NOTIFICATIONS_SECTION" : { "EMAIL_NOTIFICATIONS_SECTION" : {
"TITLE": "Email Notifications", "TITLE": "Email Notifications",
"NOTE": "Update your email notification preferences here", "NOTE": "Update your email notification preferences here",

View File

@@ -83,6 +83,17 @@
</div> </div>
</div> </div>
<email-notifications /> <email-notifications />
<div class="profile--settings--row row">
<div class="columns small-3 ">
<h4 class="block-title">
{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.TITLE') }}
</h4>
<p>{{ $t('PROFILE_SETTINGS.FORM.ACCESS_TOKEN.NOTE') }}</p>
</div>
<div class="columns small-9 medium-5">
<woot-code :script="currentUser.access_token"></woot-code>
</div>
</div>
<woot-submit-button <woot-submit-button
class="button nice success button--fixed-right-top" class="button nice success button--fixed-right-top"
:button-text="$t('PROFILE_SETTINGS.BTN_TEXT')" :button-text="$t('PROFILE_SETTINGS.BTN_TEXT')"

View File

@@ -0,0 +1,21 @@
# == Schema Information
#
# Table name: access_tokens
#
# id :bigint not null, primary key
# owner_type :string
# token :string
# created_at :datetime not null
# updated_at :datetime not null
# owner_id :bigint
#
# Indexes
#
# index_access_tokens_on_owner_type_and_owner_id (owner_type,owner_id)
# index_access_tokens_on_token (token) UNIQUE
#
class AccessToken < ApplicationRecord
has_secure_token :token
belongs_to :owner, polymorphic: true
end

View File

@@ -14,6 +14,7 @@ class Account < ApplicationRecord
validates :name, presence: true validates :name, presence: true
has_many :account_users, dependent: :destroy has_many :account_users, dependent: :destroy
has_many :agent_bot_inboxes, dependent: :destroy
has_many :users, through: :account_users has_many :users, through: :account_users
has_many :inboxes, dependent: :destroy has_many :inboxes, dependent: :destroy
has_many :conversations, dependent: :destroy has_many :conversations, dependent: :destroy

View File

@@ -3,7 +3,6 @@
# Table name: agent_bots # Table name: agent_bots
# #
# id :bigint not null, primary key # id :bigint not null, primary key
# auth_token :string
# description :string # description :string
# name :string # name :string
# outgoing_url :string # outgoing_url :string
@@ -12,8 +11,9 @@
# #
class AgentBot < ApplicationRecord class AgentBot < ApplicationRecord
include AccessTokenable
include Avatarable include Avatarable
has_many :agent_bot_inboxes, dependent: :destroy has_many :agent_bot_inboxes, dependent: :destroy
has_many :inboxes, through: :agent_bot_inboxes has_many :inboxes, through: :agent_bot_inboxes
has_secure_token :auth_token
end end

View File

@@ -6,6 +6,7 @@
# status :integer default("active") # status :integer default("active")
# created_at :datetime not null # created_at :datetime not null
# updated_at :datetime not null # updated_at :datetime not null
# account_id :integer
# agent_bot_id :integer # agent_bot_id :integer
# inbox_id :integer # inbox_id :integer
# #
@@ -13,8 +14,16 @@
class AgentBotInbox < ApplicationRecord class AgentBotInbox < ApplicationRecord
validates :inbox_id, presence: true validates :inbox_id, presence: true
validates :agent_bot_id, presence: true validates :agent_bot_id, presence: true
before_validation :ensure_account_id
belongs_to :inbox belongs_to :inbox
belongs_to :agent_bot belongs_to :agent_bot
belongs_to :account
enum status: { active: 0, inactive: 1 } enum status: { active: 0, inactive: 1 }
private
def ensure_account_id
self.account_id = inbox&.account_id
end
end end

View File

@@ -0,0 +1,11 @@
module AccessTokenable
extend ActiveSupport::Concern
included do
has_one :access_token, as: :owner, dependent: :destroy
after_create :create_access_token
end
def create_access_token
AccessToken.create!(owner: self)
end
end

View File

@@ -67,7 +67,9 @@ class Conversation < ApplicationRecord
end end
def toggle_status def toggle_status
# FIXME: implement state machine with aasm
self.status = open? ? :resolved : :open self.status = open? ? :resolved : :open
self.status = :open if bot?
save save
end end

View File

@@ -35,12 +35,13 @@
# #
class User < ApplicationRecord class User < ApplicationRecord
include AccessTokenable
include AvailabilityStatusable
include Avatarable
# Include default devise modules. # Include default devise modules.
include DeviseTokenAuth::Concerns::User include DeviseTokenAuth::Concerns::User
include Events::Types include Events::Types
include Pubsubable include Pubsubable
include Avatarable
include AvailabilityStatusable
include Rails.application.routes.url_helpers include Rails.application.routes.url_helpers
devise :database_authenticatable, devise :database_authenticatable,
@@ -69,7 +70,7 @@ class User < ApplicationRecord
before_validation :set_password_and_uid, on: :create before_validation :set_password_and_uid, on: :create
after_create :notify_creation after_create :notify_creation, :create_access_token
after_destroy :notify_deletion after_destroy :notify_deletion

View File

@@ -13,5 +13,6 @@ json.payload do
json.inviter_id @resource.account_user.inviter_id json.inviter_id @resource.account_user.inviter_id
json.confirmed @resource.confirmed? json.confirmed @resource.confirmed?
json.avatar_url @resource.avatar_url json.avatar_url @resource.avatar_url
json.access_token @resource.access_token&.token
end end
end end

View File

@@ -92,6 +92,9 @@ Rails.application.routes.draw do
end end
resources :webhooks, except: [:show] resources :webhooks, except: [:show]
namespace :widget do
resources :inboxes, only: [:create, :update]
end
end end
# end of account scoped api routes # end of account scoped api routes
@@ -101,7 +104,6 @@ Rails.application.routes.draw do
namespace :widget do namespace :widget do
resources :messages, only: [:index, :create, :update] resources :messages, only: [:index, :create, :update]
resources :inboxes, only: [:create, :update]
resources :inbox_members, only: [:index] resources :inbox_members, only: [:index]
end end

View File

@@ -0,0 +1,23 @@
class CreateAccessTokens < ActiveRecord::Migration[6.0]
def change
create_table :access_tokens do |t|
t.references :owner, polymorphic: true, index: true
t.string :token, index: { unique: true }
t.timestamps
end
remove_column :agent_bots, :auth_token, :string
[::User, ::AgentBot].each do |access_tokenable|
generate_access_tokens(access_tokenable)
end
end
def generate_access_tokens(access_tokenable)
access_tokenable.find_in_batches do |record_batch|
record_batch.each do |record|
record.create_access_token if record.access_token.blank?
end
end
end
end

View File

@@ -0,0 +1,10 @@
class AddAccountIdToAgentBotInboxes < ActiveRecord::Migration[6.0]
def change
add_column :agent_bot_inboxes, :account_id, :integer, index: true
AgentBotInbox.all.each do |agent_bot_inbox|
agent_bot_inbox.account_id = agent_bot_inbox.inbox.account_id
agent_bot_inbox.save!
end
end
end

View File

@@ -10,300 +10,311 @@
# #
# It's strongly recommended that you check this file into your version control system. # It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20_200_226_194_012) do ActiveRecord::Schema.define(version: 2020_03_09_213132) do
# These are extensions that must be enabled in order to support this database # These are extensions that must be enabled in order to support this database
enable_extension 'plpgsql' enable_extension "plpgsql"
create_table 'account_users', force: :cascade do |t| create_table "access_tokens", force: :cascade do |t|
t.bigint 'account_id' t.string "owner_type"
t.bigint 'user_id' t.bigint "owner_id"
t.integer 'role', default: 0 t.string "token"
t.bigint 'inviter_id' t.datetime "created_at", precision: 6, null: false
t.datetime 'created_at', precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.datetime 'updated_at', precision: 6, null: false t.index ["owner_type", "owner_id"], name: "index_access_tokens_on_owner_type_and_owner_id"
t.index %w[account_id user_id], name: 'uniq_user_id_per_account_id', unique: true t.index ["token"], name: "index_access_tokens_on_token", unique: true
t.index ['account_id'], name: 'index_account_users_on_account_id'
t.index ['user_id'], name: 'index_account_users_on_user_id'
end end
create_table 'accounts', id: :serial, force: :cascade do |t| create_table "account_users", force: :cascade do |t|
t.string 'name', null: false t.bigint "account_id"
t.datetime 'created_at', null: false t.bigint "user_id"
t.datetime 'updated_at', null: false t.integer "role", default: 0
t.bigint "inviter_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["account_id", "user_id"], name: "uniq_user_id_per_account_id", unique: true
t.index ["account_id"], name: "index_account_users_on_account_id"
t.index ["user_id"], name: "index_account_users_on_user_id"
end end
create_table 'active_storage_attachments', force: :cascade do |t| create_table "accounts", id: :serial, force: :cascade do |t|
t.string 'name', null: false t.string "name", null: false
t.string 'record_type', null: false t.datetime "created_at", null: false
t.bigint 'record_id', null: false t.datetime "updated_at", null: false
t.bigint 'blob_id', null: false
t.datetime 'created_at', null: false
t.index ['blob_id'], name: 'index_active_storage_attachments_on_blob_id'
t.index %w[record_type record_id name blob_id], name: 'index_active_storage_attachments_uniqueness', unique: true
end end
create_table 'active_storage_blobs', force: :cascade do |t| create_table "active_storage_attachments", force: :cascade do |t|
t.string 'key', null: false t.string "name", null: false
t.string 'filename', null: false t.string "record_type", null: false
t.string 'content_type' t.bigint "record_id", null: false
t.text 'metadata' t.bigint "blob_id", null: false
t.bigint 'byte_size', null: false t.datetime "created_at", null: false
t.string 'checksum', null: false t.index ["blob_id"], name: "index_active_storage_attachments_on_blob_id"
t.datetime 'created_at', null: false t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true
t.index ['key'], name: 'index_active_storage_blobs_on_key', unique: true
end end
create_table 'agent_bot_inboxes', force: :cascade do |t| create_table "active_storage_blobs", force: :cascade do |t|
t.integer 'inbox_id' t.string "key", null: false
t.integer 'agent_bot_id' t.string "filename", null: false
t.integer 'status', default: 0 t.string "content_type"
t.datetime 'created_at', precision: 6, null: false t.text "metadata"
t.datetime 'updated_at', precision: 6, null: false t.bigint "byte_size", null: false
t.string "checksum", null: false
t.datetime "created_at", null: false
t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true
end end
create_table 'agent_bots', force: :cascade do |t| create_table "agent_bot_inboxes", force: :cascade do |t|
t.string 'name' t.integer "inbox_id"
t.string 'description' t.integer "agent_bot_id"
t.string 'outgoing_url' t.integer "status", default: 0
t.string 'auth_token' t.datetime "created_at", precision: 6, null: false
t.datetime 'created_at', precision: 6, null: false t.datetime "updated_at", precision: 6, null: false
t.datetime 'updated_at', precision: 6, null: false t.integer "account_id"
end end
create_table 'attachments', id: :serial, force: :cascade do |t| create_table "agent_bots", force: :cascade do |t|
t.integer 'file_type', default: 0 t.string "name"
t.string 'external_url' t.string "description"
t.float 'coordinates_lat', default: 0.0 t.string "outgoing_url"
t.float 'coordinates_long', default: 0.0 t.datetime "created_at", precision: 6, null: false
t.integer 'message_id', null: false t.datetime "updated_at", precision: 6, null: false
t.integer 'account_id', null: false
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.string 'fallback_title'
t.string 'extension'
end end
create_table 'canned_responses', id: :serial, force: :cascade do |t| create_table "attachments", id: :serial, force: :cascade do |t|
t.integer 'account_id', null: false t.integer "file_type", default: 0
t.string 'short_code' t.string "external_url"
t.text 'content' t.float "coordinates_lat", default: 0.0
t.datetime 'created_at', null: false t.float "coordinates_long", default: 0.0
t.datetime 'updated_at', null: false t.integer "message_id", null: false
t.integer "account_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "fallback_title"
t.string "extension"
end end
create_table 'channel_facebook_pages', id: :serial, force: :cascade do |t| create_table "canned_responses", id: :serial, force: :cascade do |t|
t.string 'name', null: false t.integer "account_id", null: false
t.string 'page_id', null: false t.string "short_code"
t.string 'user_access_token', null: false t.text "content"
t.string 'page_access_token', null: false t.datetime "created_at", null: false
t.integer 'account_id', null: false t.datetime "updated_at", null: false
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.index %w[page_id account_id], name: 'index_channel_facebook_pages_on_page_id_and_account_id', unique: true
t.index ['page_id'], name: 'index_channel_facebook_pages_on_page_id'
end end
create_table 'channel_twitter_profiles', force: :cascade do |t| create_table "channel_facebook_pages", id: :serial, force: :cascade do |t|
t.string 'name' t.string "name", null: false
t.string 'profile_id', null: false t.string "page_id", null: false
t.string 'twitter_access_token', null: false t.string "user_access_token", null: false
t.string 'twitter_access_token_secret', null: false t.string "page_access_token", null: false
t.integer 'account_id', null: false t.integer "account_id", null: false
t.datetime 'created_at', precision: 6, null: false t.datetime "created_at", null: false
t.datetime 'updated_at', precision: 6, null: false t.datetime "updated_at", null: false
t.index ["page_id", "account_id"], name: "index_channel_facebook_pages_on_page_id_and_account_id", unique: true
t.index ["page_id"], name: "index_channel_facebook_pages_on_page_id"
end end
create_table 'channel_web_widgets', id: :serial, force: :cascade do |t| create_table "channel_twitter_profiles", force: :cascade do |t|
t.string 'website_name' t.string "name"
t.string 'website_url' t.string "profile_id", null: false
t.integer 'account_id' t.string "twitter_access_token", null: false
t.datetime 'created_at', null: false t.string "twitter_access_token_secret", null: false
t.datetime 'updated_at', null: false t.integer "account_id", null: false
t.string 'website_token' t.datetime "created_at", precision: 6, null: false
t.string 'widget_color', default: '#1f93ff' t.datetime "updated_at", precision: 6, null: false
t.index ['website_token'], name: 'index_channel_web_widgets_on_website_token', unique: true
end end
create_table 'contact_inboxes', force: :cascade do |t| create_table "channel_web_widgets", id: :serial, force: :cascade do |t|
t.bigint 'contact_id' t.string "website_name"
t.bigint 'inbox_id' t.string "website_url"
t.string 'source_id', null: false t.integer "account_id"
t.datetime 'created_at', precision: 6, null: false t.datetime "created_at", null: false
t.datetime 'updated_at', precision: 6, null: false t.datetime "updated_at", null: false
t.index ['contact_id'], name: 'index_contact_inboxes_on_contact_id' t.string "website_token"
t.index %w[inbox_id source_id], name: 'index_contact_inboxes_on_inbox_id_and_source_id', unique: true t.string "widget_color", default: "#1f93ff"
t.index ['inbox_id'], name: 'index_contact_inboxes_on_inbox_id' t.index ["website_token"], name: "index_channel_web_widgets_on_website_token", unique: true
t.index ['source_id'], name: 'index_contact_inboxes_on_source_id'
end end
create_table 'contacts', id: :serial, force: :cascade do |t| create_table "contact_inboxes", force: :cascade do |t|
t.string 'name' t.bigint "contact_id"
t.string 'email' t.bigint "inbox_id"
t.string 'phone_number' t.string "source_id", null: false
t.integer 'account_id', null: false t.datetime "created_at", precision: 6, null: false
t.datetime 'created_at', null: false t.datetime "updated_at", precision: 6, null: false
t.datetime 'updated_at', null: false t.index ["contact_id"], name: "index_contact_inboxes_on_contact_id"
t.string 'pubsub_token' t.index ["inbox_id", "source_id"], name: "index_contact_inboxes_on_inbox_id_and_source_id", unique: true
t.jsonb 'additional_attributes' t.index ["inbox_id"], name: "index_contact_inboxes_on_inbox_id"
t.index ['account_id'], name: 'index_contacts_on_account_id' t.index ["source_id"], name: "index_contact_inboxes_on_source_id"
t.index ['pubsub_token'], name: 'index_contacts_on_pubsub_token', unique: true
end end
create_table 'conversations', id: :serial, force: :cascade do |t| create_table "contacts", id: :serial, force: :cascade do |t|
t.integer 'account_id', null: false t.string "name"
t.integer 'inbox_id', null: false t.string "email"
t.integer 'status', default: 0, null: false t.string "phone_number"
t.integer 'assignee_id' t.integer "account_id", null: false
t.datetime 'created_at', null: false t.datetime "created_at", null: false
t.datetime 'updated_at', null: false t.datetime "updated_at", null: false
t.bigint 'contact_id' t.string "pubsub_token"
t.integer 'display_id', null: false t.jsonb "additional_attributes"
t.datetime 'user_last_seen_at' t.index ["account_id"], name: "index_contacts_on_account_id"
t.datetime 'agent_last_seen_at' t.index ["pubsub_token"], name: "index_contacts_on_pubsub_token", unique: true
t.boolean 'locked', default: false
t.jsonb 'additional_attributes'
t.bigint 'contact_inbox_id'
t.index %w[account_id display_id], name: 'index_conversations_on_account_id_and_display_id', unique: true
t.index ['account_id'], name: 'index_conversations_on_account_id'
t.index ['contact_inbox_id'], name: 'index_conversations_on_contact_inbox_id'
end end
create_table 'inbox_members', id: :serial, force: :cascade do |t| create_table "conversations", id: :serial, force: :cascade do |t|
t.integer 'user_id', null: false t.integer "account_id", null: false
t.integer 'inbox_id', null: false t.integer "inbox_id", null: false
t.datetime 'created_at', null: false t.integer "status", default: 0, null: false
t.datetime 'updated_at', null: false t.integer "assignee_id"
t.index ['inbox_id'], name: 'index_inbox_members_on_inbox_id' t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.bigint "contact_id"
t.integer "display_id", null: false
t.datetime "user_last_seen_at"
t.datetime "agent_last_seen_at"
t.boolean "locked", default: false
t.jsonb "additional_attributes"
t.bigint "contact_inbox_id"
t.index ["account_id", "display_id"], name: "index_conversations_on_account_id_and_display_id", unique: true
t.index ["account_id"], name: "index_conversations_on_account_id"
t.index ["contact_inbox_id"], name: "index_conversations_on_contact_inbox_id"
end end
create_table 'inboxes', id: :serial, force: :cascade do |t| create_table "inbox_members", id: :serial, force: :cascade do |t|
t.integer 'channel_id', null: false t.integer "user_id", null: false
t.integer 'account_id', null: false t.integer "inbox_id", null: false
t.string 'name', null: false t.datetime "created_at", null: false
t.datetime 'created_at', null: false t.datetime "updated_at", null: false
t.datetime 'updated_at', null: false t.index ["inbox_id"], name: "index_inbox_members_on_inbox_id"
t.string 'channel_type'
t.boolean 'enable_auto_assignment', default: true
t.index ['account_id'], name: 'index_inboxes_on_account_id'
end end
create_table 'messages', id: :serial, force: :cascade do |t| create_table "inboxes", id: :serial, force: :cascade do |t|
t.text 'content' t.integer "channel_id", null: false
t.integer 'account_id', null: false t.integer "account_id", null: false
t.integer 'inbox_id', null: false t.string "name", null: false
t.integer 'conversation_id', null: false t.datetime "created_at", null: false
t.integer 'message_type', null: false t.datetime "updated_at", null: false
t.datetime 'created_at', null: false t.string "channel_type"
t.datetime 'updated_at', null: false t.boolean "enable_auto_assignment", default: true
t.boolean 'private', default: false t.index ["account_id"], name: "index_inboxes_on_account_id"
t.integer 'user_id'
t.integer 'status', default: 0
t.string 'source_id'
t.integer 'content_type', default: 0
t.json 'content_attributes', default: {}
t.bigint 'contact_id'
t.index ['contact_id'], name: 'index_messages_on_contact_id'
t.index ['conversation_id'], name: 'index_messages_on_conversation_id'
t.index ['source_id'], name: 'index_messages_on_source_id'
end end
create_table 'notification_settings', force: :cascade do |t| create_table "messages", id: :serial, force: :cascade do |t|
t.integer 'account_id' t.text "content"
t.integer 'user_id' t.integer "account_id", null: false
t.integer 'email_flags', default: 0, null: false t.integer "inbox_id", null: false
t.datetime 'created_at', precision: 6, null: false t.integer "conversation_id", null: false
t.datetime 'updated_at', precision: 6, null: false t.integer "message_type", null: false
t.index %w[account_id user_id], name: 'by_account_user', unique: true t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.boolean "private", default: false
t.integer "user_id"
t.integer "status", default: 0
t.string "source_id"
t.integer "content_type", default: 0
t.json "content_attributes", default: {}
t.bigint "contact_id"
t.index ["contact_id"], name: "index_messages_on_contact_id"
t.index ["conversation_id"], name: "index_messages_on_conversation_id"
t.index ["source_id"], name: "index_messages_on_source_id"
end end
create_table 'subscriptions', id: :serial, force: :cascade do |t| create_table "notification_settings", force: :cascade do |t|
t.string 'pricing_version' t.integer "account_id"
t.integer 'account_id' t.integer "user_id"
t.datetime 'expiry' t.integer "email_flags", default: 0, null: false
t.string 'billing_plan', default: 'trial' t.datetime "created_at", precision: 6, null: false
t.string 'stripe_customer_id' t.datetime "updated_at", precision: 6, null: false
t.datetime 'created_at', null: false t.index ["account_id", "user_id"], name: "by_account_user", unique: true
t.datetime 'updated_at', null: false
t.integer 'state', default: 0
t.boolean 'payment_source_added', default: false
end end
create_table 'taggings', id: :serial, force: :cascade do |t| create_table "subscriptions", id: :serial, force: :cascade do |t|
t.integer 'tag_id' t.string "pricing_version"
t.string 'taggable_type' t.integer "account_id"
t.integer 'taggable_id' t.datetime "expiry"
t.string 'tagger_type' t.string "billing_plan", default: "trial"
t.integer 'tagger_id' t.string "stripe_customer_id"
t.string 'context', limit: 128 t.datetime "created_at", null: false
t.datetime 'created_at' t.datetime "updated_at", null: false
t.index ['context'], name: 'index_taggings_on_context' t.integer "state", default: 0
t.index %w[tag_id taggable_id taggable_type context tagger_id tagger_type], name: 'taggings_idx', unique: true t.boolean "payment_source_added", default: false
t.index ['tag_id'], name: 'index_taggings_on_tag_id'
t.index %w[taggable_id taggable_type context], name: 'index_taggings_on_taggable_id_and_taggable_type_and_context'
t.index %w[taggable_id taggable_type tagger_id context], name: 'taggings_idy'
t.index ['taggable_id'], name: 'index_taggings_on_taggable_id'
t.index ['taggable_type'], name: 'index_taggings_on_taggable_type'
t.index %w[tagger_id tagger_type], name: 'index_taggings_on_tagger_id_and_tagger_type'
t.index ['tagger_id'], name: 'index_taggings_on_tagger_id'
end end
create_table 'tags', id: :serial, force: :cascade do |t| create_table "taggings", id: :serial, force: :cascade do |t|
t.string 'name' t.integer "tag_id"
t.integer 'taggings_count', default: 0 t.string "taggable_type"
t.index ['name'], name: 'index_tags_on_name', unique: true t.integer "taggable_id"
t.string "tagger_type"
t.integer "tagger_id"
t.string "context", limit: 128
t.datetime "created_at"
t.index ["context"], name: "index_taggings_on_context"
t.index ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "taggings_idx", unique: true
t.index ["tag_id"], name: "index_taggings_on_tag_id"
t.index ["taggable_id", "taggable_type", "context"], name: "index_taggings_on_taggable_id_and_taggable_type_and_context"
t.index ["taggable_id", "taggable_type", "tagger_id", "context"], name: "taggings_idy"
t.index ["taggable_id"], name: "index_taggings_on_taggable_id"
t.index ["taggable_type"], name: "index_taggings_on_taggable_type"
t.index ["tagger_id", "tagger_type"], name: "index_taggings_on_tagger_id_and_tagger_type"
t.index ["tagger_id"], name: "index_taggings_on_tagger_id"
end end
create_table 'telegram_bots', id: :serial, force: :cascade do |t| create_table "tags", id: :serial, force: :cascade do |t|
t.string 'name' t.string "name"
t.string 'auth_key' t.integer "taggings_count", default: 0
t.integer 'account_id' t.index ["name"], name: "index_tags_on_name", unique: true
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
end end
create_table 'users', id: :serial, force: :cascade do |t| create_table "telegram_bots", id: :serial, force: :cascade do |t|
t.string 'provider', default: 'email', null: false t.string "name"
t.string 'uid', default: '', null: false t.string "auth_key"
t.string 'encrypted_password', default: '', null: false t.integer "account_id"
t.string 'reset_password_token' t.datetime "created_at", null: false
t.datetime 'reset_password_sent_at' t.datetime "updated_at", null: false
t.datetime 'remember_created_at'
t.integer 'sign_in_count', default: 0, null: false
t.datetime 'current_sign_in_at'
t.datetime 'last_sign_in_at'
t.string 'current_sign_in_ip'
t.string 'last_sign_in_ip'
t.string 'confirmation_token'
t.datetime 'confirmed_at'
t.datetime 'confirmation_sent_at'
t.string 'unconfirmed_email'
t.string 'name', null: false
t.string 'nickname'
t.string 'email'
t.json 'tokens'
t.datetime 'created_at', null: false
t.datetime 'updated_at', null: false
t.string 'pubsub_token'
t.index ['email'], name: 'index_users_on_email'
t.index ['pubsub_token'], name: 'index_users_on_pubsub_token', unique: true
t.index ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true
t.index %w[uid provider], name: 'index_users_on_uid_and_provider', unique: true
end end
create_table 'webhooks', force: :cascade do |t| create_table "users", id: :serial, force: :cascade do |t|
t.integer 'account_id' t.string "provider", default: "email", null: false
t.integer 'inbox_id' t.string "uid", default: "", null: false
t.string 'url' t.string "encrypted_password", default: "", null: false
t.datetime 'created_at', precision: 6, null: false t.string "reset_password_token"
t.datetime 'updated_at', precision: 6, null: false t.datetime "reset_password_sent_at"
t.integer 'webhook_type', default: 0 t.datetime "remember_created_at"
t.integer "sign_in_count", default: 0, null: false
t.datetime "current_sign_in_at"
t.datetime "last_sign_in_at"
t.string "current_sign_in_ip"
t.string "last_sign_in_ip"
t.string "confirmation_token"
t.datetime "confirmed_at"
t.datetime "confirmation_sent_at"
t.string "unconfirmed_email"
t.string "name", null: false
t.string "nickname"
t.string "email"
t.json "tokens"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "pubsub_token"
t.index ["email"], name: "index_users_on_email"
t.index ["pubsub_token"], name: "index_users_on_pubsub_token", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
t.index ["uid", "provider"], name: "index_users_on_uid_and_provider", unique: true
end end
add_foreign_key 'account_users', 'accounts' create_table "webhooks", force: :cascade do |t|
add_foreign_key 'account_users', 'users' t.integer "account_id"
add_foreign_key 'active_storage_attachments', 'active_storage_blobs', column: 'blob_id' t.integer "inbox_id"
add_foreign_key 'contact_inboxes', 'contacts' t.string "url"
add_foreign_key 'contact_inboxes', 'inboxes' t.datetime "created_at", precision: 6, null: false
add_foreign_key 'conversations', 'contact_inboxes' t.datetime "updated_at", precision: 6, null: false
add_foreign_key 'messages', 'contacts' t.integer "webhook_type", default: 0
end
add_foreign_key "account_users", "accounts"
add_foreign_key "account_users", "users"
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "contact_inboxes", "contacts"
add_foreign_key "contact_inboxes", "inboxes"
add_foreign_key "conversations", "contact_inboxes"
add_foreign_key "messages", "contacts"
end end

View File

@@ -103,7 +103,7 @@
"eslint --fix", "eslint --fix",
"git add" "git add"
], ],
"*.rb": [ "!(*schema).rb": [
"rubocop -a", "rubocop -a",
"git add" "git add"
], ],

View File

@@ -0,0 +1,60 @@
require 'rails_helper'
RSpec.describe 'API Base', type: :request do
let!(:account) { create(:account) }
let!(:user) { create(:user, account: account) }
describe 'request with api_access_token for user' do
context 'when it is an invalid api_access_token' do
it 'returns unauthorized' do
get '/api/v1/profile',
headers: { api_access_token: 'invalid' },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is a valid api_access_token' do
it 'returns current user information' do
get '/api/v1/profile',
headers: { api_access_token: user.access_token.token },
as: :json
expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body)
expect(json_response['id']).to eq(user.id)
expect(json_response['email']).to eq(user.email)
end
end
end
describe 'request with api_access_token for bot' do
let!(:agent_bot) { create(:agent_bot) }
let!(:inbox) { create(:inbox, account: account) }
let!(:conversation) { create(:conversation, account: account, inbox: inbox, assignee: user, status: 'bot') }
context 'when it is an unauthorized url' do
it 'returns unauthorized' do
get '/api/v1/profile',
headers: { api_access_token: agent_bot.access_token.token },
as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is a accessible url' do
it 'returns success' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: { api_access_token: agent_bot.access_token.token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('open')
end
end
end
end

View File

@@ -1,10 +1,11 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe 'Conversation Messages API', type: :request do RSpec.describe 'Conversation Messages API', type: :request do
let(:account) { create(:account) } let!(:account) { create(:account) }
describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/messages' do describe 'POST /api/v1/accounts/{account.id}/conversations/<id>/messages' do
let(:conversation) { create(:conversation, account: account) } let!(:inbox) { create(:inbox, account: account) }
let!(:conversation) { create(:conversation, inbox: inbox, account: account) }
context 'when it is an unauthenticated user' do context 'when it is an unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
@@ -30,6 +31,24 @@ RSpec.describe 'Conversation Messages API', type: :request do
expect(conversation.messages.first.content).to eq(params[:message]) expect(conversation.messages.first.content).to eq(params[:message])
end end
end end
context 'when it is an authenticated agent bot' do
let!(:agent_bot) { create(:agent_bot) }
it 'creates a new outgoing message' do
create(:agent_bot_inbox, inbox: inbox, agent_bot: agent_bot)
params = { message: 'test-message' }
post api_v1_account_conversation_messages_url(account_id: account.id, conversation_id: conversation.display_id),
params: params,
headers: { api_access_token: agent_bot.access_token.token },
as: :json
expect(response).to have_http_status(:success)
expect(conversation.messages.count).to eq(1)
expect(conversation.messages.first.content).to eq(params[:message])
end
end
end end
describe 'GET /api/v1/accounts/{account.id}/conversations/:id/messages' do describe 'GET /api/v1/accounts/{account.id}/conversations/:id/messages' do

View File

@@ -80,6 +80,17 @@ RSpec.describe 'Conversations API', type: :request do
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('resolved') expect(conversation.reload.status).to eq('resolved')
end end
it 'toggles the conversation status to open from bot' do
conversation.update!(status: 'bot')
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_status",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.status).to eq('open')
end
end end
end end

View File

@@ -1,17 +1,17 @@
require 'rails_helper' require 'rails_helper'
RSpec.describe '/api/v1/widget/inboxes', type: :request do RSpec.describe '/api/v1/accounts/{account.id}/widget/inboxes', type: :request do
let(:account) { create(:account) } let(:account) { create(:account) }
let(:inbox) { create(:inbox, account: account) } let(:inbox) { create(:inbox, account: account) }
let(:admin) { create(:user, account: account, role: :administrator) } let(:admin) { create(:user, account: account, role: :administrator) }
let(:agent) { create(:user, account: account, role: :agent) } let(:agent) { create(:user, account: account, role: :agent) }
describe 'POST /api/v1/widget/inboxes' do describe 'POST /api/v1/accounts/{account.id}/widget/inboxes' do
let(:params) { { website: { website_name: 'test', website_url: 'test.com', widget_color: '#eaeaea' } } } let(:params) { { website: { website_name: 'test', website_url: 'test.com', widget_color: '#eaeaea' } } }
context 'when unauthenticated user' do context 'when unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
post '/api/v1/widget/inboxes', params: params post "/api/v1/accounts/#{account.id}/widget/inboxes", params: params
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
@@ -19,7 +19,7 @@ RSpec.describe '/api/v1/widget/inboxes', type: :request do
context 'when user is logged in' do context 'when user is logged in' do
context 'with user as administrator' do context 'with user as administrator' do
it 'creates inbox and returns website_token' do it 'creates inbox and returns website_token' do
post '/api/v1/widget/inboxes', params: params, headers: admin.create_new_auth_token post "/api/v1/accounts/#{account.id}/widget/inboxes", params: params, headers: admin.create_new_auth_token
expect(response).to have_http_status(:success) expect(response).to have_http_status(:success)
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
@@ -31,7 +31,7 @@ RSpec.describe '/api/v1/widget/inboxes', type: :request do
context 'with user as agent' do context 'with user as agent' do
it 'returns unauthorized' do it 'returns unauthorized' do
post '/api/v1/widget/inboxes', post "/api/v1/accounts/#{account.id}/widget/inboxes",
params: params, params: params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token
@@ -41,12 +41,12 @@ RSpec.describe '/api/v1/widget/inboxes', type: :request do
end end
end end
describe 'PATCH /api/v1/widget/inboxes/:id' do describe 'PATCH /api/v1/accounts/{account.id}/widget/inboxes/:id' do
let(:update_params) { { website: { widget_color: '#eaeaea' } } } let(:update_params) { { website: { widget_color: '#eaeaea' } } }
context 'when unauthenticated user' do context 'when unauthenticated user' do
it 'returns unauthorized' do it 'returns unauthorized' do
patch "/api/v1/widget/inboxes/#{inbox.channel_id}", params: update_params patch "/api/v1/accounts/#{account.id}/widget/inboxes/#{inbox.channel_id}", params: update_params
expect(response).to have_http_status(:unauthorized) expect(response).to have_http_status(:unauthorized)
end end
end end
@@ -54,7 +54,7 @@ RSpec.describe '/api/v1/widget/inboxes', type: :request do
context 'when user is logged in' do context 'when user is logged in' do
context 'with user as administrator' do context 'with user as administrator' do
it 'updates website channel' do it 'updates website channel' do
patch "/api/v1/widget/inboxes/#{inbox.channel_id}", patch "/api/v1/accounts/#{account.id}/widget/inboxes/#{inbox.channel_id}",
params: update_params, params: update_params,
headers: admin.create_new_auth_token headers: admin.create_new_auth_token
@@ -67,7 +67,7 @@ RSpec.describe '/api/v1/widget/inboxes', type: :request do
context 'with user as agent' do context 'with user as agent' do
it 'returns unauthorized' do it 'returns unauthorized' do
patch "/api/v1/widget/inboxes/#{inbox.channel_id}", patch "/api/v1/accounts/#{account.id}/widget/inboxes/#{inbox.channel_id}",
params: update_params, params: update_params,
headers: agent.create_new_auth_token headers: agent.create_new_auth_token