diff --git a/app/controllers/concerns/ensure_current_account_helper.rb b/app/controllers/concerns/ensure_current_account_helper.rb
index dccc64350..eb781dbfe 100644
--- a/app/controllers/concerns/ensure_current_account_helper.rb
+++ b/app/controllers/concerns/ensure_current_account_helper.rb
@@ -8,6 +8,8 @@ module EnsureCurrentAccountHelper
def ensure_current_account
account = Account.find(params[:account_id])
+ ensure_account_is_active?(account)
+
if current_user
account_accessible_for_user?(account)
elsif @resource.is_a?(AgentBot)
@@ -25,4 +27,8 @@ module EnsureCurrentAccountHelper
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
+
+ def ensure_account_is_active?(account)
+ render_unauthorized('Account is suspended') unless account.active?
+ end
end
diff --git a/app/controllers/concerns/website_token_helper.rb b/app/controllers/concerns/website_token_helper.rb
index 0158a4107..ea53a26ad 100644
--- a/app/controllers/concerns/website_token_helper.rb
+++ b/app/controllers/concerns/website_token_helper.rb
@@ -5,7 +5,9 @@ module WebsiteTokenHelper
def set_web_widget
@web_widget = ::Channel::WebWidget.find_by!(website_token: permitted_params[:website_token])
- @current_account = @web_widget.account
+ @current_account = @web_widget.inbox.account
+
+ render json: { error: 'Account is suspended' }, status: :unauthorized unless @current_account.active?
end
def set_contact
diff --git a/app/controllers/widgets_controller.rb b/app/controllers/widgets_controller.rb
index 653d3360f..9639b28b2 100644
--- a/app/controllers/widgets_controller.rb
+++ b/app/controllers/widgets_controller.rb
@@ -4,6 +4,7 @@ class WidgetsController < ActionController::Base
before_action :set_global_config
before_action :set_web_widget
+ before_action :ensure_account_is_active
before_action :set_token
before_action :set_contact
before_action :build_contact
@@ -46,6 +47,10 @@ class WidgetsController < ActionController::Base
@contact = @contact_inbox.contact
end
+ def ensure_account_is_active
+ render json: { error: 'Account is suspended' }, status: :unauthorized unless @web_widget.inbox.account.active?
+ end
+
def additional_attributes
if @web_widget.inbox.account.feature_enabled?('ip_lookup')
{ created_at_ip: request.remote_ip }
diff --git a/app/dashboards/account_dashboard.rb b/app/dashboards/account_dashboard.rb
index 408589597..0dfe7bbcd 100644
--- a/app/dashboards/account_dashboard.rb
+++ b/app/dashboards/account_dashboard.rb
@@ -17,6 +17,7 @@ class AccountDashboard < Administrate::BaseDashboard
users: CountField,
conversations: CountField,
locale: Field::Select.with_options(collection: LANGUAGES_CONFIG.map { |_x, y| y[:iso_639_1_code] }),
+ status: Field::Select.with_options(collection: [%w[Active active], %w[Suspended suspended]]),
account_users: Field::HasMany
}.merge(enterprise_attribute_types).freeze
@@ -31,6 +32,7 @@ class AccountDashboard < Administrate::BaseDashboard
locale
users
conversations
+ status
].freeze
# SHOW_PAGE_ATTRIBUTES
@@ -42,6 +44,7 @@ class AccountDashboard < Administrate::BaseDashboard
created_at
updated_at
locale
+ status
conversations
account_users
] + enterprise_show_page_attributes).freeze
@@ -53,6 +56,7 @@ class AccountDashboard < Administrate::BaseDashboard
FORM_ATTRIBUTES = (%i[
name
locale
+ status
] + enterprise_form_attributes).freeze
# COLLECTION_FILTERS
diff --git a/app/javascript/dashboard/helper/routeHelpers.js b/app/javascript/dashboard/helper/routeHelpers.js
new file mode 100644
index 000000000..c0abccc00
--- /dev/null
+++ b/app/javascript/dashboard/helper/routeHelpers.js
@@ -0,0 +1,51 @@
+export const getCurrentAccount = ({ accounts } = {}, accountId) => {
+ return accounts.find(account => account.id === accountId);
+};
+
+export const getUserRole = ({ accounts } = {}, accountId) => {
+ const currentAccount = getCurrentAccount({ accounts }, accountId) || {};
+ return currentAccount.role || null;
+};
+
+export const routeIsAccessibleFor = (route, role, roleWiseRoutes) => {
+ return roleWiseRoutes[role].includes(route);
+};
+
+const validateActiveAccountRoutes = (to, user, roleWiseRoutes) => {
+ // If the current account is active, then check for the route permissions
+ const accountDashboardURL = `accounts/${to.params.accountId}/dashboard`;
+
+ // If the user is trying to access suspended route, redirect them to dashboard
+ if (to.name === 'account_suspended') {
+ return accountDashboardURL;
+ }
+
+ const userRole = getUserRole(user, Number(to.params.accountId));
+ const isAccessible = routeIsAccessibleFor(to.name, userRole, roleWiseRoutes);
+ // If the route is not accessible for the user, return to dashboard screen
+ return isAccessible ? null : accountDashboardURL;
+};
+
+export const validateLoggedInRoutes = (to, user, roleWiseRoutes) => {
+ const currentAccount = getCurrentAccount(user, Number(to.params.accountId));
+
+ // If current account is missing, either user does not have
+ // access to the account or the account is deleted, return to login screen
+ if (!currentAccount) {
+ return `app/login`;
+ }
+
+ const isCurrentAccountActive = currentAccount.status === 'active';
+
+ if (isCurrentAccountActive) {
+ return validateActiveAccountRoutes(to, user, roleWiseRoutes);
+ }
+
+ // If the current account is not active, then redirect the user to the suspended screen
+ if (to.name !== 'account_suspended') {
+ return `accounts/${to.params.accountId}/suspended`;
+ }
+
+ // Proceed to the route if none of the above conditions are met
+ return null;
+};
diff --git a/app/javascript/dashboard/helper/specs/routeHelpers.spec.js b/app/javascript/dashboard/helper/specs/routeHelpers.spec.js
new file mode 100644
index 000000000..f3417111a
--- /dev/null
+++ b/app/javascript/dashboard/helper/specs/routeHelpers.spec.js
@@ -0,0 +1,96 @@
+import {
+ getCurrentAccount,
+ getUserRole,
+ routeIsAccessibleFor,
+ validateLoggedInRoutes,
+} from '../routeHelpers';
+
+describe('#getCurrentAccount', () => {
+ it('should return the current account', () => {
+ expect(getCurrentAccount({ accounts: [{ id: 1 }] }, 1)).toEqual({ id: 1 });
+ expect(getCurrentAccount({ accounts: [] }, 1)).toEqual(undefined);
+ });
+});
+
+describe('#getUserRole', () => {
+ it('should return the current role', () => {
+ expect(
+ getUserRole({ accounts: [{ id: 1, role: 'administrator' }] }, 1)
+ ).toEqual('administrator');
+ expect(getUserRole({ accounts: [] }, 1)).toEqual(null);
+ });
+});
+
+describe('#routeIsAccessibleFor', () => {
+ it('should return the correct access', () => {
+ const roleWiseRoutes = { agent: ['conversations'], admin: ['billing'] };
+ expect(routeIsAccessibleFor('billing', 'agent', roleWiseRoutes)).toEqual(
+ false
+ );
+ expect(routeIsAccessibleFor('billing', 'admin', roleWiseRoutes)).toEqual(
+ true
+ );
+ });
+});
+
+describe('#validateLoggedInRoutes', () => {
+ describe('when account access is missing', () => {
+ it('should return the login route', () => {
+ expect(
+ validateLoggedInRoutes(
+ { params: { accountId: 1 } },
+ { accounts: [] },
+ {}
+ )
+ ).toEqual(`app/login`);
+ });
+ });
+ describe('when account access is available', () => {
+ describe('when account is suspended', () => {
+ it('return suspended route', () => {
+ expect(
+ validateLoggedInRoutes(
+ { name: 'conversations', params: { accountId: 1 } },
+ { accounts: [{ id: 1, role: 'agent', status: 'suspended' }] },
+ { agent: ['conversations'] }
+ )
+ ).toEqual(`accounts/1/suspended`);
+ });
+ });
+ describe('when account is active', () => {
+ describe('when route is accessible', () => {
+ it('returns null (no action required)', () => {
+ expect(
+ validateLoggedInRoutes(
+ { name: 'conversations', params: { accountId: 1 } },
+ { accounts: [{ id: 1, role: 'agent', status: 'active' }] },
+ { agent: ['conversations'] }
+ )
+ ).toEqual(null);
+ });
+ });
+ describe('when route is not accessible', () => {
+ it('returns dashboard url', () => {
+ expect(
+ validateLoggedInRoutes(
+ { name: 'conversations', params: { accountId: 1 } },
+ { accounts: [{ id: 1, role: 'agent', status: 'active' }] },
+ { admin: ['conversations'], agent: [] }
+ )
+ ).toEqual(`accounts/1/dashboard`);
+ });
+ });
+ describe('when route is suspended route', () => {
+ it('returns dashboard url', () => {
+ expect(
+ validateLoggedInRoutes(
+ { name: 'account_suspended', params: { accountId: 1 } },
+ { accounts: [{ id: 1, role: 'agent', status: 'active' }] },
+ { agent: ['account_suspended'] }
+ )
+ ).toEqual(`accounts/1/dashboard`);
+ });
+ });
+ });
+ });
+});
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index 73b02b89d..0db8e393d 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -120,7 +120,11 @@
"APP_GLOBAL": {
"TRIAL_MESSAGE": "days trial remaining.",
"TRAIL_BUTTON": "Buy Now",
- "DELETED_USER": "Deleted User"
+ "DELETED_USER": "Deleted User",
+ "ACCOUNT_SUSPENDED": {
+ "TITLE": "Account Suspended",
+ "MESSAGE": "Your account is suspended. Please reach out to the support team for more information."
+ }
},
"COMPONENTS": {
"CODE": {
@@ -199,7 +203,7 @@
},
"BILLING_SETTINGS": {
"TITLE": "Billing",
- "CURRENT_PLAN" : {
+ "CURRENT_PLAN": {
"TITLE": "Current Plan",
"PLAN_NOTE": "You are currently subscribed to the **%{plan}** plan with **%{quantity}** licenses"
},
diff --git a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js
index 343b11c27..8a7ebee72 100644
--- a/app/javascript/dashboard/routes/dashboard/dashboard.routes.js
+++ b/app/javascript/dashboard/routes/dashboard/dashboard.routes.js
@@ -6,6 +6,8 @@ import { routes as notificationRoutes } from './notifications/routes';
import { frontendURL } from '../../helper/URLHelper';
import helpcenterRoutes from './helpcenter/helpcenter.routes';
+const Suspended = () => import('./suspended/Index');
+
export default {
routes: [
...helpcenterRoutes.routes,
@@ -19,5 +21,11 @@ export default {
...notificationRoutes,
],
},
+ {
+ path: frontendURL('accounts/:accountId/suspended'),
+ name: 'account_suspended',
+ roles: ['administrator', 'agent'],
+ component: Suspended,
+ },
],
};
diff --git a/app/javascript/dashboard/routes/dashboard/suspended/Index.vue b/app/javascript/dashboard/routes/dashboard/suspended/Index.vue
new file mode 100644
index 000000000..c25a8a7eb
--- /dev/null
+++ b/app/javascript/dashboard/routes/dashboard/suspended/Index.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
diff --git a/app/javascript/dashboard/routes/index.js b/app/javascript/dashboard/routes/index.js
index 2557538ec..586f14804 100644
--- a/app/javascript/dashboard/routes/index.js
+++ b/app/javascript/dashboard/routes/index.js
@@ -6,6 +6,7 @@ import authRoute from './auth/auth.routes';
import dashboard from './dashboard/dashboard.routes';
import login from './login/login.routes';
import store from '../store';
+import { validateLoggedInRoutes } from '../helper/routeHelpers';
const routes = [...login.routes, ...dashboard.routes, ...authRoute.routes];
@@ -14,11 +15,6 @@ window.roleWiseRoutes = {
administrator: [],
};
-const getUserRole = ({ accounts } = {}, accountId) => {
- const currentAccount = accounts.find(account => account.id === accountId);
- return currentAccount ? currentAccount.role : null;
-};
-
// generateRoleWiseRoute - updates window object with agent/admin route
const generateRoleWiseRoute = route => {
route.forEach(element => {
@@ -47,10 +43,6 @@ const authIgnoreRoutes = [
'auth_password_edit',
];
-function routeIsAccessibleFor(route, role) {
- return window.roleWiseRoutes[role].includes(route);
-}
-
const routeValidators = [
{
protected: false,
@@ -68,12 +60,8 @@ const routeValidators = [
{
protected: true,
loggedIn: true,
- handler: (to, getters) => {
- const user = getters.getCurrentUser;
- const userRole = getUserRole(user, Number(to.params.accountId));
- const isAccessible = routeIsAccessibleFor(to.name, userRole);
- return isAccessible ? null : `accounts/${to.params.accountId}/dashboard`;
- },
+ handler: (to, getters) =>
+ validateLoggedInRoutes(to, getters.getCurrentUser, window.roleWiseRoutes),
},
{
protected: false,
diff --git a/app/javascript/dashboard/routes/index.spec.js b/app/javascript/dashboard/routes/index.spec.js
index ae8b21d46..b9bf3c251 100644
--- a/app/javascript/dashboard/routes/index.spec.js
+++ b/app/javascript/dashboard/routes/index.spec.js
@@ -26,7 +26,7 @@ describe('#validateAuthenticateRoutePermission', () => {
getCurrentUser: {
account_id: 1,
id: 1,
- accounts: [{ id: 1, role: 'admin' }],
+ accounts: [{ id: 1, role: 'admin', status: 'active' }],
},
};
validateAuthenticateRoutePermission(to, from, next, { getters });
@@ -72,7 +72,7 @@ describe('#validateAuthenticateRoutePermission', () => {
getCurrentUser: {
account_id: 1,
id: 1,
- accounts: [{ id: 1, role: 'agent' }],
+ accounts: [{ id: 1, role: 'agent', status: 'active' }],
},
};
validateAuthenticateRoutePermission(to, from, next, { getters });
@@ -90,7 +90,7 @@ describe('#validateAuthenticateRoutePermission', () => {
getCurrentUser: {
account_id: 1,
id: 1,
- accounts: [{ id: 1, role: 'agent' }],
+ accounts: [{ id: 1, role: 'agent', status: 'active' }],
},
};
validateAuthenticateRoutePermission(to, from, next, { getters });
diff --git a/app/models/account.rb b/app/models/account.rb
index d7c4718b0..9a719f881 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -11,10 +11,15 @@
# locale :integer default("en")
# name :string not null
# settings_flags :integer default(0), not null
+# status :integer default("active")
# support_email :string(100)
# created_at :datetime not null
# updated_at :datetime not null
#
+# Indexes
+#
+# index_accounts_on_status (status)
+#
class Account < ApplicationRecord
# used for single column multi flags
@@ -79,6 +84,7 @@ class Account < ApplicationRecord
has_flags ACCOUNT_SETTINGS_FLAGS.merge(column: 'settings_flags').merge(DEFAULT_QUERY_SETTING)
enum locale: LANGUAGES_CONFIG.map { |key, val| [val[:iso_639_1_code], key] }.to_h
+ enum status: { active: 0, suspended: 1 }
before_validation :validate_limit_keys
after_create_commit :notify_creation
diff --git a/app/views/api/v1/models/_account.json.jbuilder b/app/views/api/v1/models/_account.json.jbuilder
index ddf366597..3d258265a 100644
--- a/app/views/api/v1/models/_account.json.jbuilder
+++ b/app/views/api/v1/models/_account.json.jbuilder
@@ -13,3 +13,4 @@ json.id @account.id
json.locale @account.locale
json.name @account.name
json.support_email @account.support_email
+json.status @account.status
diff --git a/app/views/api/v1/models/_user.json.jbuilder b/app/views/api/v1/models/_user.json.jbuilder
index 4fc857ae7..4e7075e39 100644
--- a/app/views/api/v1/models/_user.json.jbuilder
+++ b/app/views/api/v1/models/_user.json.jbuilder
@@ -20,6 +20,7 @@ json.accounts do
json.array! resource.account_users do |account_user|
json.id account_user.account_id
json.name account_user.account.name
+ json.status account_user.account.status
json.active_at account_user.active_at
json.role account_user.role
# the actual availability user has configured
diff --git a/db/migrate/20220802133722_add_status_to_accounts.rb b/db/migrate/20220802133722_add_status_to_accounts.rb
new file mode 100644
index 000000000..b94e8ee74
--- /dev/null
+++ b/db/migrate/20220802133722_add_status_to_accounts.rb
@@ -0,0 +1,6 @@
+class AddStatusToAccounts < ActiveRecord::Migration[6.1]
+ def change
+ add_column :accounts, :status, :integer, default: 0
+ add_index :accounts, :status
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 3a78d5413..17271fa65 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2022_07_20_123615) do
+ActiveRecord::Schema.define(version: 2022_08_02_133722) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
@@ -54,6 +54,8 @@ ActiveRecord::Schema.define(version: 2022_07_20_123615) do
t.integer "auto_resolve_duration"
t.jsonb "limits", default: {}
t.jsonb "custom_attributes", default: {}
+ t.integer "status", default: 0
+ t.index ["status"], name: "index_accounts_on_status"
end
create_table "action_mailbox_inbound_emails", force: :cascade do |t|
diff --git a/spec/controllers/api/base_controller_spec.rb b/spec/controllers/api/base_controller_spec.rb
index 969b83f68..c1dc781e9 100644
--- a/spec/controllers/api/base_controller_spec.rb
+++ b/spec/controllers/api/base_controller_spec.rb
@@ -56,5 +56,17 @@ RSpec.describe 'API Base', type: :request do
expect(conversation.reload.status).to eq('open')
end
end
+
+ context 'when the account is suspended' do
+ it 'returns 401 unauthorized' do
+ account.update!(status: :suspended)
+
+ post "/api/v1/accounts/#{account.id}/canned_responses",
+ headers: { api_access_token: user.access_token.token },
+ as: :json
+
+ expect(response).to have_http_status(:unauthorized)
+ end
+ end
end
end
diff --git a/spec/controllers/api/v1/widget/configs_controller_spec.rb b/spec/controllers/api/v1/widget/configs_controller_spec.rb
index 8101885bd..4c4cef2c9 100644
--- a/spec/controllers/api/v1/widget/configs_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/configs_controller_spec.rb
@@ -47,6 +47,17 @@ RSpec.describe '/api/v1/widget/config', type: :request do
expect(response_data.keys).to include(*response_keys)
expect(response_data['contact']['pubsub_token']).to eq(contact_inbox.pubsub_token)
end
+
+ it 'returns 401 if account is suspended' do
+ account.update!(status: :suspended)
+
+ post '/api/v1/widget/config',
+ params: params,
+ headers: { 'X-Auth-Token' => token },
+ as: :json
+
+ expect(response).to have_http_status(:unauthorized)
+ end
end
context 'with correct website token and invalid X-Auth-Token' do
diff --git a/spec/controllers/widgets_controller_spec.rb b/spec/controllers/widgets_controller_spec.rb
index 07624e03e..8b955b6f6 100644
--- a/spec/controllers/widgets_controller_spec.rb
+++ b/spec/controllers/widgets_controller_spec.rb
@@ -2,7 +2,7 @@ require 'rails_helper'
describe '/widget', type: :request do
let(:account) { create(:account) }
- let(:web_widget) { create(:channel_widget) }
+ let(:web_widget) { create(:channel_widget, account: account) }
let(:contact) { create(:contact, account: account) }
let(:contact_inbox) { create(:contact_inbox, contact: contact, inbox: web_widget.inbox) }
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
@@ -25,5 +25,13 @@ describe '/widget', type: :request do
get widget_url
expect(response).to have_http_status(:not_found)
end
+
+ it 'returns 401 if the account is suspended' do
+ account.update!(status: :suspended)
+
+ get widget_url(website_token: web_widget.website_token)
+ expect(response).to have_http_status(:unauthorized)
+ expect(response.body).to include('Account is suspended')
+ end
end
end
diff --git a/spec/factories/accounts.rb b/spec/factories/accounts.rb
index aa8a9b7e6..968962cff 100644
--- a/spec/factories/accounts.rb
+++ b/spec/factories/accounts.rb
@@ -3,6 +3,7 @@
FactoryBot.define do
factory :account do
sequence(:name) { |n| "Account #{n}" }
+ status { 'active' }
custom_email_domain_enabled { false }
domain { 'test.com' }
support_email { 'support@test.com' }