feat: Add Platform APIs (#1456)

This commit is contained in:
Sojan Jose 2021-01-14 20:35:22 +05:30 committed by GitHub
parent 75c2a7cb2f
commit 7542330d61
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 688 additions and 20 deletions

View file

@ -48,6 +48,7 @@ Rails/ApplicationController:
- 'app/controllers/dashboard_controller.rb'
- 'app/controllers/widget_tests_controller.rb'
- 'app/controllers/widgets_controller.rb'
- 'app/controllers/platform_controller.rb'
Style/ClassAndModuleChildren:
EnforcedStyle: compact
Exclude:

View file

@ -6,13 +6,6 @@
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 1
# Configuration parameters: EnforcedStyle.
# SupportedStyles: native, lf, crlf
Layout/EndOfLine:
Exclude:
- 'deploy/after_restart.rb'
# Offense count: 1
Lint/DuplicateMethods:
Exclude:

View file

@ -23,7 +23,7 @@ class ApplicationController < ActionController::Base
render_unauthorized('You are not authorized to do this action')
ensure
# to address the thread variable leak issues in Puma/Thin webserver
Current.user = nil
Current.reset
end
def set_current_user

View file

@ -0,0 +1,29 @@
class Platform::Api::V1::AccountUsersController < PlatformController
before_action :set_resource
before_action :validate_platform_app_permissible
def index
render json: @resource.account_users
end
def create
@account_user = @resource.account_users.find_or_initialize_by(user_id: account_user_params[:user_id])
@account_user.update!(account_user_params)
render json: @account_user
end
def destroy
@resource.account_users.find_by(user_id: account_user_params[:user_id])&.destroy
head :ok
end
private
def set_resource
@resource = Account.find(params[:account_id])
end
def account_user_params
params.permit(:user_id, :role)
end
end

View file

@ -0,0 +1,32 @@
class Platform::Api::V1::AccountsController < PlatformController
def create
@resource = Account.new(account_params)
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
render json: @resource
end
def show
render json: @resource
end
def update
@resource.update!(account_params)
render json: @resource
end
def destroy
# TODO: obfusicate account
head :ok
end
private
def set_resource
@resource = Account.find(params[:id])
end
def account_params
params.permit(:name)
end
end

View file

@ -0,0 +1,43 @@
class Platform::Api::V1::UsersController < PlatformController
# ref: https://stackoverflow.com/a/45190318/939299
# set resource is called for other actions already in platform controller
# we want to add login to that chain as well
before_action(only: [:login]) { set_resource }
before_action(only: [:login]) { validate_platform_app_permissible }
def create
@resource = (User.find_by(email: user_params[:email]) || User.new(user_params))
@resource.confirm
@resource.save!
@platform_app.platform_app_permissibles.find_or_create_by(permissible: @resource)
render json: @resource
end
def login
render json: { url: "#{ENV['FRONTEND_URL']}/app/login?email=#{@resource.email}&sso_auth_token=#{@resource.generate_sso_auth_token}" }
end
def show
render json: @resource
end
def update
@resource.update!(user_params)
render json: @resource
end
def destroy
# TODO: obfusicate user
head :ok
end
private
def set_resource
@resource = User.find(params[:id])
end
def user_params
params.permit(:name, :email, :password)
end
end

View file

@ -0,0 +1,37 @@
class PlatformController < ActionController::Base
protect_from_forgery with: :null_session
before_action :ensure_access_token
before_action :set_platform_app
before_action :set_resource, only: [:update, :show, :destroy]
before_action :validate_platform_app_permissible, only: [:update, :show, :destroy]
def show; end
def update; end
def destroy; end
private
def ensure_access_token
token = request.headers[:api_access_token] || request.headers[:HTTP_API_ACCESS_TOKEN]
@access_token = AccessToken.find_by(token: token) if token.present?
end
def set_platform_app
@platform_app = @access_token.owner if @access_token && @access_token.owner.is_a?(PlatformApp)
render json: { error: 'Invalid access_token' }, status: :unauthorized if @platform_app.blank?
end
def set_resource
# set @resource in your controller
raise 'Overwrite this method your controller'
end
def validate_platform_app_permissible
return if @platform_app.platform_app_permissibles.find_by(permissible: @resource)
render json: { error: 'Non permissible resource' }, status: :unauthorized
end
end

View file

@ -32,7 +32,6 @@ class Inbox < ApplicationRecord
belongs_to :account
# TODO: should add associations for the channel types
belongs_to :channel, polymorphic: true, dependent: :destroy
has_many :contact_inboxes, dependent: :destroy

View file

@ -0,0 +1,16 @@
# == Schema Information
#
# Table name: platform_apps
#
# id :bigint not null, primary key
# name :string not null
# created_at :datetime not null
# updated_at :datetime not null
#
class PlatformApp < ApplicationRecord
include AccessTokenable
validates :name, presence: true
has_many :platform_app_permissibles, dependent: :destroy
end

View file

@ -0,0 +1,26 @@
# == Schema Information
#
# Table name: platform_app_permissibles
#
# id :bigint not null, primary key
# permissible_type :string not null
# created_at :datetime not null
# updated_at :datetime not null
# permissible_id :bigint not null
# platform_app_id :bigint not null
#
# Indexes
#
# index_platform_app_permissibles_on_permissibles (permissible_type,permissible_id)
# index_platform_app_permissibles_on_platform_app_id (platform_app_id)
# unique_permissibles_index (platform_app_id,permissible_id,permissible_type) UNIQUE
#
class PlatformAppPermissible < ApplicationRecord
include AccessTokenable
validates :platform_app, presence: true
validates :platform_app_id, uniqueness: { scope: [:permissible_id, :permissible_type] }
belongs_to :platform_app
belongs_to :permissible, polymorphic: true, dependent: :destroy
end

View file

@ -155,13 +155,25 @@ Rails.application.routes.draw do
end
end
namespace :twitter do
resource :authorization, only: [:create]
resource :callback, only: [:show]
end
namespace :twilio do
resources :callback, only: [:create]
# ----------------------------------------------------------------------
# Routes for platform APIs
namespace :platform, defaults: { format: 'json' } do
namespace :api do
namespace :v1 do
resources :users, only: [:create, :show, :update, :destroy] do
member do
get :login
end
end
resources :accounts, only: [:create, :show, :update, :destroy] do
resources :account_users, only: [:index, :create] do
collection do
delete :destroy
end
end
end
end
end
end
# ----------------------------------------------------------------------
@ -173,14 +185,19 @@ Rails.application.routes.draw do
end
# ----------------------------------------------------------------------
# Routes for social integrations
# Routes for channel integrations
mount Facebook::Messenger::Server, at: 'bot'
get 'webhooks/twitter', to: 'api/v1/webhooks#twitter_crc'
post 'webhooks/twitter', to: 'api/v1/webhooks#twitter_events'
# ----------------------------------------------------------------------
# Routes for testing
resources :widget_tests, only: [:index] unless Rails.env.production?
namespace :twitter do
resource :authorization, only: [:create]
resource :callback, only: [:show]
end
namespace :twilio do
resources :callback, only: [:create]
end
# ----------------------------------------------------------------------
# Routes for external service verifications
@ -216,4 +233,8 @@ Rails.application.routes.draw do
# Routes for swagger docs
get '/swagger/*path', to: 'swagger#respond'
get '/swagger', to: 'swagger#respond'
# ----------------------------------------------------------------------
# Routes for testing
resources :widget_tests, only: [:index] unless Rails.env.production?
end

View file

@ -0,0 +1,8 @@
class CreatePlatformApps < ActiveRecord::Migration[6.0]
def change
create_table :platform_apps do |t|
t.string :name, null: false
t.timestamps
end
end
end

View file

@ -0,0 +1,11 @@
class CreatePlatformAppPermissibles < ActiveRecord::Migration[6.0]
def change
create_table :platform_app_permissibles do |t|
t.references :platform_app, index: true, null: false
t.references :permissible, null: false, polymorphic: true, index: { name: :index_platform_app_permissibles_on_permissibles }
t.timestamps
end
add_index :platform_app_permissibles, [:platform_app_id, :permissible_id, :permissible_type], unique: true, name: 'unique_permissibles_index'
end
end

View file

@ -10,6 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2021_01_09_211805) do
# These are extensions that must be enabled in order to support this database
@ -428,6 +429,23 @@ ActiveRecord::Schema.define(version: 2021_01_09_211805) do
t.index ["user_id"], name: "index_notifications_on_user_id"
end
create_table "platform_app_permissibles", force: :cascade do |t|
t.bigint "platform_app_id", null: false
t.string "permissible_type", null: false
t.bigint "permissible_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["permissible_type", "permissible_id"], name: "index_platform_app_permissibles_on_permissibles"
t.index ["platform_app_id", "permissible_id", "permissible_type"], name: "unique_permissibles_index", unique: true
t.index ["platform_app_id"], name: "index_platform_app_permissibles_on_platform_app_id"
end
create_table "platform_apps", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "super_admins", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false

View file

@ -7,4 +7,10 @@ module Current
super
Time.zone = account.timezone
end
def self.reset
Current.user = nil
Current.account = nil
Current.account_user = nil
end
end

View file

@ -0,0 +1,93 @@
require 'rails_helper'
RSpec.describe 'Platform Account Users API', type: :request do
let!(:account) { create(:account) }
describe 'GET /platform/api/v1/accounts/{account_id}/account_users' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
get "/platform/api/v1/accounts/#{account.id}/account_users"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
let!(:account_user) { create(:account_user, account: account) }
it 'returns all the account users for the account' do
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
get "/platform/api/v1/accounts/#{account.id}/account_users",
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(account_user.id.to_s)
end
end
end
describe 'POST /platform/api/v1/accounts/{account_id}/account_users' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
post "/platform/api/v1/accounts/#{account.id}/account_users"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'creates a new account user for the account' do
user = create(:user)
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
post "/platform/api/v1/accounts/#{account.id}/account_users",
params: { user_id: user.id, role: 'administrator' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data['user_id']).to eq(user.id)
end
it 'updates the new account user for the account' do
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
account_user = create(:account_user, account: account, role: 'agent')
post "/platform/api/v1/accounts/#{account.id}/account_users",
params: { user_id: account_user.user_id, role: 'administrator' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data['role']).to eq('administrator')
end
end
end
describe 'DELETE /platform/api/v1/accounts/{account_id}/account_users' do
let(:account_user) { create(:account_user, account: account, role: 'agent') }
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
delete "/platform/api/v1/accounts/#{account.id}/account_users", params: { user_id: account_user.user_id }
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns deletes the account user' do
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
delete "/platform/api/v1/accounts/#{account.id}/account_users", params: { user_id: account_user.user_id },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
expect(account.account_users.count).to eq 0
end
end
end
end

View file

@ -0,0 +1,107 @@
require 'rails_helper'
RSpec.describe 'Platform Accounts API', type: :request do
let!(:account) { create(:account) }
describe 'POST /platform/api/v1/accounts' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
post '/platform/api/v1/accounts'
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
post '/platform/api/v1/accounts', params: { name: 'Test Account' },
headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'creates an account when and its permissible relationship' do
post '/platform/api/v1/accounts', params: { name: 'Test Account' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include('Test Account')
expect(platform_app.platform_app_permissibles.first.permissible.name).to eq('Test Account')
end
end
end
describe 'GET /platform/api/v1/accounts/{account_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
get "/platform/api/v1/accounts/#{account.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
get "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
get "/platform/api/v1/accounts/#{account.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'shows an account when its permissible object' do
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
get "/platform/api/v1/accounts/#{account.id}",
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
expect(response.body).to include(account.name)
end
end
end
describe 'PATCH /platform/api/v1/accounts/{account_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
patch "/platform/api/v1/accounts/#{account.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
patch "/platform/api/v1/accounts/#{account.id}", params: { name: 'Test Account' },
headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
patch "/platform/api/v1/accounts/#{account.id}", params: { name: 'Test Account' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates an account when its permissible object' do
create(:platform_app_permissible, platform_app: platform_app, permissible: account)
patch "/platform/api/v1/accounts/#{account.id}", params: { name: 'Test Account' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
expect(account.reload.name).to eq('Test Account')
end
end
end
end

View file

@ -0,0 +1,154 @@
require 'rails_helper'
RSpec.describe 'Platform Users API', type: :request do
let!(:user) { create(:user) }
describe 'GET /platform/api/v1/users/{user_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
get "/platform/api/v1/users/#{user.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
get "/platform/api/v1/users/#{user.id}", headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
get "/platform/api/v1/users/#{user.id}", headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'shows a user when its permissible object' do
create(:platform_app_permissible, platform_app: platform_app, permissible: user)
get "/platform/api/v1/users/#{user.id}",
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data['email']).to eq(user.email)
end
end
end
describe 'GET /platform/api/v1/users/{user_id}/login' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
get "/platform/api/v1/users/#{user.id}/login"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
get "/platform/api/v1/users/#{user.id}/login", headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
get "/platform/api/v1/users/#{user.id}/login", headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'return login link for user' do
create(:platform_app_permissible, platform_app: platform_app, permissible: user)
get "/platform/api/v1/users/#{user.id}/login",
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data['url']).to include('sso_auth_token')
end
end
end
describe 'POST /platform/api/v1/users/' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
post '/platform/api/v1/users'
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
post '/platform/api/v1/users/', headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'creates a new user and permissible for the user' do
post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'password123' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data['email']).to eq('test@test.com')
expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id']
end
it 'fetch existing user and creates permissible for the user' do
create(:user, name: 'old test', email: 'test@test.com')
post '/platform/api/v1/users/', params: { name: 'test', email: 'test@test.com', password: 'password123' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data['name']).to eq('old test')
expect(platform_app.platform_app_permissibles.first.permissible_id).to eq data['id']
end
end
end
describe 'PATCH /platform/api/v1/users/{user_id}' do
context 'when it is an unauthenticated platform app' do
it 'returns unauthorized' do
patch "/platform/api/v1/users/#{user.id}"
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an invalid platform app token' do
it 'returns unauthorized' do
patch "/platform/api/v1/users/#{user.id}", headers: { api_access_token: 'invalid' }, as: :json
expect(response).to have_http_status(:unauthorized)
end
end
context 'when it is an authenticated platform app' do
let(:platform_app) { create(:platform_app) }
it 'returns unauthorized when its not a permissible object' do
patch "/platform/api/v1/users/#{user.id}", params: { name: 'test' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:unauthorized)
end
it 'updates the user' do
create(:platform_app_permissible, platform_app: platform_app, permissible: user)
patch "/platform/api/v1/users/#{user.id}", params: { name: 'test123' },
headers: { api_access_token: platform_app.access_token.token }, as: :json
expect(response).to have_http_status(:success)
data = JSON.parse(response.body)
expect(data['name']).to eq('test123')
end
end
end
end

View file

@ -0,0 +1,5 @@
FactoryBot.define do
factory :platform_app do
name { Faker::Book.name }
end
end

View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :platform_app_permissible do
platform_app
permissible { create(:user) }
end
end

View file

@ -1,8 +1,13 @@
require 'rails_helper'
require Rails.root.join 'spec/models/concerns/access_tokenable_spec.rb'
RSpec.describe AgentBot, type: :model do
describe 'associations' do
it { is_expected.to have_many(:agent_bot_inboxes) }
it { is_expected.to have_many(:inboxes) }
end
describe 'concerns' do
it_behaves_like 'access_tokenable'
end
end

View file

@ -0,0 +1,9 @@
require 'rails_helper'
shared_examples_for 'access_tokenable' do
let(:obj) { create(described_class.to_s.underscore) }
it 'creates access token on create' do
expect(obj.access_token).not_to eq(nil)
end
end

View file

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe PlatformAppPermissible do
let!(:platform_app_permissible) { create(:platform_app_permissible) }
context 'with validations' do
it { is_expected.to validate_presence_of(:platform_app) }
end
context 'with associations' do
it { is_expected.to belong_to(:platform_app) }
it { is_expected.to belong_to(:permissible) }
end
describe 'with factories' do
it { expect(platform_app_permissible).present? }
end
end

View file

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'rails_helper'
require Rails.root.join 'spec/models/concerns/access_tokenable_spec.rb'
RSpec.describe PlatformApp do
let(:platform_app) { create(:platform_app) }
context 'with validations' do
it { is_expected.to validate_presence_of(:name) }
end
context 'with associations' do
it { is_expected.to have_many(:platform_app_permissibles) }
end
describe 'with concerns' do
it_behaves_like 'access_tokenable'
end
describe 'with factories' do
it { expect(platform_app).present? }
end
end

View file

@ -1,6 +1,7 @@
# frozen_string_literal: true
require 'rails_helper'
require Rails.root.join 'spec/models/concerns/access_tokenable_spec.rb'
RSpec.describe User do
let!(:user) { create(:user) }
@ -20,6 +21,10 @@ RSpec.describe User do
it { is_expected.to have_many(:events) }
end
describe 'concerns' do
it_behaves_like 'access_tokenable'
end
describe 'pubsub_token' do
before { user.update(name: Faker::Name.name) }